import { PrismaPg } from '@prisma/adapter-pg'; import { randomUUID } from 'crypto'; import { readFileSync } from 'fs'; import { resolve } from 'path'; import { z } from 'zod/v4'; import { PrismaClient } from '../src/generated/prisma/client.js'; const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); const prisma = new PrismaClient({ adapter }); const PointGeometrySchema = z.object({ type: z.literal('Point'), coordinates: z.tuple([z.number(), z.number()]), }); const MultiPolygonGeometrySchema = z.object({ type: z.literal('MultiPolygon'), coordinates: z.array(z.array(z.array(z.tuple([z.number(), z.number()])))), }); const RegionPropertiesSchema = z.object({ name: z.string(), code: z.string(), iso: z.string(), }); const DatacenterPropertiesSchema = z.object({ name: z.string(), operator: z.string(), capacity_mw: z.number(), status: z.string(), year_opened: z.number(), region: z.string(), }); const RegionFeatureSchema = z.object({ type: z.literal('Feature'), geometry: MultiPolygonGeometrySchema, properties: RegionPropertiesSchema, }); const DatacenterFeatureSchema = z.object({ type: z.literal('Feature'), geometry: PointGeometrySchema, properties: DatacenterPropertiesSchema, }); const RegionCollectionSchema = z.object({ type: z.literal('FeatureCollection'), features: z.array(RegionFeatureSchema), }); const DatacenterCollectionSchema = z.object({ type: z.literal('FeatureCollection'), features: z.array(DatacenterFeatureSchema), }); const PowerPlantPropertiesSchema = z.object({ Plant_Name: z.string(), Plant_Code: z.number(), Utility_Na: z.string(), State: z.string(), PrimSource: z.string(), Total_MW: z.number(), }); const PowerPlantFeatureSchema = z.object({ type: z.literal('Feature'), geometry: PointGeometrySchema, properties: PowerPlantPropertiesSchema, }); const PowerPlantCollectionSchema = z.object({ type: z.literal('FeatureCollection'), features: z.array(PowerPlantFeatureSchema), }); const AIMilestoneSchema = z.object({ date: z.string(), title: z.string(), description: z.string(), category: z.string(), }); const GeopoliticalEventSeedSchema = z.object({ title: z.string(), description: z.string(), category: z.string(), severity: z.string(), timestamp: z.string(), sourceUrl: z.string().nullable().optional(), }); function readAndParse(relativePath: string, schema: z.ZodType): T { const fullPath = resolve(import.meta.dirname, '..', relativePath); const raw: unknown = JSON.parse(readFileSync(fullPath, 'utf-8')); return schema.parse(raw); } async function seedGridRegions() { console.log('Seeding grid regions...'); const geojson = readAndParse('data/grid-regions.geojson', RegionCollectionSchema); // Delete only static seed data that doesn't have time-series FK references await prisma.$executeRawUnsafe('DELETE FROM datacenters'); // Upsert grid_regions by code to preserve FK references from time-series tables for (const feature of geojson.features) { const id = randomUUID(); const geojsonStr = JSON.stringify(feature.geometry); await prisma.$executeRawUnsafe( `INSERT INTO grid_regions (id, name, code, iso, boundary, created_at) VALUES ($1::uuid, $2, $3, $4, ST_GeomFromGeoJSON($5)::geography, NOW()) ON CONFLICT (code) DO UPDATE SET name = EXCLUDED.name, iso = EXCLUDED.iso, boundary = EXCLUDED.boundary`, id, feature.properties.name, feature.properties.code, feature.properties.iso, geojsonStr, ); console.log(` Upserted region: ${feature.properties.code}`); } } async function seedDatacenters() { console.log('Seeding datacenters...'); const geojson = readAndParse('data/datacenters.geojson', DatacenterCollectionSchema); // Get all region codes from DB const regions = await prisma.$queryRawUnsafe>( 'SELECT id, code FROM grid_regions', ); const regionMap = new Map(regions.map(r => [r.code, r.id])); let inserted = 0; let skipped = 0; for (const feature of geojson.features) { const props = feature.properties; const regionId = regionMap.get(props.region); if (!regionId) { // Try to find the region by spatial containment const [lng, lat] = feature.geometry.coordinates; const spatialMatch = await prisma.$queryRawUnsafe>( `SELECT id, code FROM grid_regions WHERE ST_Contains(boundary::geometry, ST_SetSRID(ST_MakePoint($1, $2), 4326)) LIMIT 1`, lng, lat, ); if (spatialMatch.length > 0) { const match = spatialMatch[0]!; const id = randomUUID(); const geojsonStr = JSON.stringify(feature.geometry); await prisma.$executeRawUnsafe( `INSERT INTO datacenters (id, name, operator, location, capacity_mw, status, year_opened, region_id, created_at) VALUES ($1::uuid, $2, $3, ST_GeomFromGeoJSON($4)::geography, $5, $6, $7, $8::uuid, NOW())`, id, props.name, props.operator, geojsonStr, props.capacity_mw, props.status, props.year_opened, match.id, ); console.log(` Inserted DC: ${props.name} (spatial match -> ${match.code})`); inserted++; } else { console.log(` Skipped DC: ${props.name} (no matching region for "${props.region}")`); skipped++; } continue; } const id = randomUUID(); const geojsonStr = JSON.stringify(feature.geometry); await prisma.$executeRawUnsafe( `INSERT INTO datacenters (id, name, operator, location, capacity_mw, status, year_opened, region_id, created_at) VALUES ($1::uuid, $2, $3, ST_GeomFromGeoJSON($4)::geography, $5, $6, $7, $8::uuid, NOW())`, id, props.name, props.operator, geojsonStr, props.capacity_mw, props.status, props.year_opened, regionId, ); console.log(` Inserted DC: ${props.name} (${props.region})`); inserted++; } console.log(` Total: ${inserted.toString()} inserted, ${skipped.toString()} skipped`); } /** Normalize ArcGIS PrimSource strings to consistent capitalized fuel types. */ function normalizeFuelType(primSource: string): string { const lower = primSource.toLowerCase().trim(); if (lower === 'natural gas') return 'Natural Gas'; if (lower === 'coal') return 'Coal'; if (lower === 'nuclear') return 'Nuclear'; if (lower.includes('hydro') || lower === 'hydroelectric conventional' || lower === 'pumped storage') return 'Hydroelectric'; if (lower === 'wind') return 'Wind'; if (lower === 'solar') return 'Solar'; if (lower.includes('petroleum') || lower === 'petroleum') return 'Petroleum'; if (lower.includes('biomass') || lower === 'wood' || lower === 'wood and wood derived fuels') return 'Biomass'; if (lower === 'geothermal') return 'Geothermal'; return 'Other'; } async function seedPowerPlants() { console.log('Seeding power plants...'); const geojson = readAndParse('data/power-plants.geojson', PowerPlantCollectionSchema); let upserted = 0; let skipped = 0; for (const feature of geojson.features) { const props = feature.properties; const [lng, lat] = feature.geometry.coordinates; // Skip features with invalid coordinates if (!lng || !lat || lat === 0 || lng === 0) { skipped++; continue; } const id = randomUUID(); const fuelType = normalizeFuelType(props.PrimSource); await prisma.$executeRawUnsafe( `INSERT INTO power_plants (id, plant_code, name, operator, location, capacity_mw, fuel_type, state, created_at) VALUES ($1::uuid, $2, $3, $4, ST_SetSRID(ST_MakePoint($5, $6), 4326)::geography, $7, $8, $9, NOW()) ON CONFLICT (plant_code) DO UPDATE SET name = EXCLUDED.name, operator = EXCLUDED.operator, location = EXCLUDED.location, capacity_mw = EXCLUDED.capacity_mw, fuel_type = EXCLUDED.fuel_type, state = EXCLUDED.state`, id, props.Plant_Code, props.Plant_Name, props.Utility_Na, lng, lat, props.Total_MW, fuelType, props.State, ); upserted++; } console.log(` Total: ${upserted.toString()} upserted, ${skipped.toString()} skipped`); } function validateAIMilestones() { console.log('Validating AI milestones...'); const milestones = readAndParse('data/ai-milestones.json', z.array(AIMilestoneSchema)); console.log(` Valid JSON with ${milestones.length.toString()} milestones`); for (const m of milestones) { if (isNaN(Date.parse(m.date))) { throw new Error(`Invalid date in milestone: ${m.date}`); } } console.log(' All milestones valid'); } async function seedGeopoliticalEvents() { console.log('Seeding geopolitical events...'); const events = readAndParse('data/geopolitical-events.json', z.array(GeopoliticalEventSeedSchema)); // Clear existing events await prisma.$executeRawUnsafe('DELETE FROM geopolitical_events'); let inserted = 0; for (const event of events) { const id = randomUUID(); await prisma.$executeRawUnsafe( `INSERT INTO geopolitical_events (id, title, description, category, severity, timestamp, source_url, created_at) VALUES ($1::uuid, $2, $3, $4, $5, $6::timestamptz, $7, NOW())`, id, event.title, event.description, event.category, event.severity, new Date(event.timestamp), event.sourceUrl ?? null, ); inserted++; } console.log(` Inserted ${inserted} geopolitical events`); } async function main() { console.log('Starting seed...\n'); await seedGridRegions(); console.log(''); await seedDatacenters(); console.log(''); await seedPowerPlants(); console.log(''); validateAIMilestones(); await seedGeopoliticalEvents(); console.log(''); // Print summary console.log('\n--- Seed Summary ---'); const regionCount = await prisma.$queryRawUnsafe>( 'SELECT count(*) as count FROM grid_regions', ); const dcCount = await prisma.$queryRawUnsafe>('SELECT count(*) as count FROM datacenters'); const ppCount = await prisma.$queryRawUnsafe>('SELECT count(*) as count FROM power_plants'); const eventCount = await prisma.$queryRawUnsafe>( 'SELECT count(*) as count FROM geopolitical_events', ); console.log(`Grid regions: ${regionCount[0]!.count.toString()}`); console.log(`Datacenters: ${dcCount[0]!.count.toString()}`); console.log(`Power plants: ${ppCount[0]!.count.toString()}`); console.log(`Geopolitical events: ${eventCount[0]!.count.toString()}`); // Show sample spatial data const sample = await prisma.$queryRawUnsafe>( 'SELECT name, ST_AsText(location) as location_text FROM datacenters LIMIT 3', ); console.log('\nSample datacenter locations:'); for (const s of sample) { console.log(` ${s.name}: ${s.location_text}`); } console.log('\nSeed complete!'); } main() .catch((e: unknown) => { console.error('Seed failed:', e); process.exit(1); }) .finally(() => { void prisma.$disconnect(); });