230 lines
6.9 KiB
TypeScript

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 AIMilestoneSchema = z.object({
date: z.string(),
title: z.string(),
description: z.string(),
category: z.string(),
});
function readAndParse<T>(relativePath: string, schema: z.ZodType<T>): 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 existing data (order matters for foreign keys)
await prisma.$executeRawUnsafe('DELETE FROM datacenters');
await prisma.$executeRawUnsafe('DELETE FROM electricity_prices');
await prisma.$executeRawUnsafe('DELETE FROM generation_mix');
await prisma.$executeRawUnsafe('DELETE FROM grid_regions');
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())`,
id,
feature.properties.name,
feature.properties.code,
feature.properties.iso,
geojsonStr,
);
console.log(` Inserted 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<Array<{ id: string; code: string }>>(
'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<Array<{ id: string; code: string }>>(
`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`);
}
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 main() {
console.log('Starting seed...\n');
await seedGridRegions();
console.log('');
await seedDatacenters();
console.log('');
validateAIMilestones();
// Print summary
console.log('\n--- Seed Summary ---');
const regionCount = await prisma.$queryRawUnsafe<Array<{ count: bigint }>>(
'SELECT count(*) as count FROM grid_regions',
);
const dcCount = await prisma.$queryRawUnsafe<Array<{ count: bigint }>>('SELECT count(*) as count FROM datacenters');
console.log(`Grid regions: ${regionCount[0]!.count.toString()}`);
console.log(`Datacenters: ${dcCount[0]!.count.toString()}`);
// Show sample spatial data
const sample = await prisma.$queryRawUnsafe<Array<{ name: string; location_text: string }>>(
'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();
});