- Server: Express.js with 13 API route files (auth, regulations, contacts, calendar, truck stops, bridges, weigh stations, alerts, load board, escort locator, orders, documents, contributions) - Database: PostgreSQL with Prisma ORM, 15 models covering all modules - Auth: JWT + bcrypt with role-based access control (driver/carrier/escort/admin) - Geospatial: Haversine distance filtering on truck stops, bridges, escorts - Seed script: Imports all existing mock data (51 states, contacts, equipment, truck stops, bridges, weigh stations, alerts, seasonal restrictions) - Frontend: All 10 data-driven pages now fetch from /api instead of mock-data.js - API client (api.js): Compatibility layer that transforms API responses to match existing frontend rendering code, minimizing page-level changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
347 lines
13 KiB
JavaScript
347 lines
13 KiB
JavaScript
// =====================================================================
|
|
// Database Seed Script
|
|
// Reads mock data from the existing frontend JS files and inserts
|
|
// into PostgreSQL via Prisma.
|
|
// Run with: npm run db:seed
|
|
// =====================================================================
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const prisma = require('../config/db');
|
|
|
|
// Load the mock data files by evaluating them in a controlled context
|
|
function loadMockData(filename) {
|
|
const filepath = path.join(__dirname, '..', '..', '..', filename);
|
|
const code = fs.readFileSync(filepath, 'utf-8');
|
|
const context = {};
|
|
// Execute the JS to populate the const variables
|
|
const fn = new Function(code + '\nreturn { ' +
|
|
'MOCK_STATE_REGULATIONS: typeof MOCK_STATE_REGULATIONS !== "undefined" ? MOCK_STATE_REGULATIONS : undefined,' +
|
|
'MOCK_LOAD_BOARD: typeof MOCK_LOAD_BOARD !== "undefined" ? MOCK_LOAD_BOARD : undefined,' +
|
|
'MOCK_ESCORT_OPERATORS: typeof MOCK_ESCORT_OPERATORS !== "undefined" ? MOCK_ESCORT_OPERATORS : undefined,' +
|
|
'MOCK_STATE_CONTACTS: typeof MOCK_STATE_CONTACTS !== "undefined" ? MOCK_STATE_CONTACTS : undefined,' +
|
|
'MOCK_STATE_EQUIPMENT: typeof MOCK_STATE_EQUIPMENT !== "undefined" ? MOCK_STATE_EQUIPMENT : undefined,' +
|
|
'MOCK_TRUCK_STOPS: typeof MOCK_TRUCK_STOPS !== "undefined" ? MOCK_TRUCK_STOPS : undefined,' +
|
|
'MOCK_BRIDGE_CLEARANCES: typeof MOCK_BRIDGE_CLEARANCES !== "undefined" ? MOCK_BRIDGE_CLEARANCES : undefined,' +
|
|
'MOCK_WEIGH_STATIONS: typeof MOCK_WEIGH_STATIONS !== "undefined" ? MOCK_WEIGH_STATIONS : undefined,' +
|
|
'MOCK_ROUTE_CONDITIONS: typeof MOCK_ROUTE_CONDITIONS !== "undefined" ? MOCK_ROUTE_CONDITIONS : undefined,' +
|
|
'MOCK_WEATHER_ALERTS: typeof MOCK_WEATHER_ALERTS !== "undefined" ? MOCK_WEATHER_ALERTS : undefined,' +
|
|
'MOCK_SEASONAL_RESTRICTIONS: typeof MOCK_SEASONAL_RESTRICTIONS !== "undefined" ? MOCK_SEASONAL_RESTRICTIONS : undefined,' +
|
|
'MOCK_DOCUMENTS: typeof MOCK_DOCUMENTS !== "undefined" ? MOCK_DOCUMENTS : undefined' +
|
|
' };');
|
|
return fn();
|
|
}
|
|
|
|
async function seed() {
|
|
console.log('🌱 Seeding database...\n');
|
|
|
|
// Load mock data from both files
|
|
const data1 = loadMockData('mock-data.js');
|
|
const data2 = loadMockData('mock-data-extended.js');
|
|
|
|
// Clear existing data in reverse dependency order
|
|
console.log(' 🗑️ Clearing existing data...');
|
|
await prisma.contribution.deleteMany();
|
|
await prisma.document.deleteMany();
|
|
await prisma.order.deleteMany();
|
|
await prisma.load.deleteMany();
|
|
await prisma.escortProfile.deleteMany();
|
|
await prisma.user.deleteMany();
|
|
await prisma.alert.deleteMany();
|
|
await prisma.weighStation.deleteMany();
|
|
await prisma.bridge.deleteMany();
|
|
await prisma.truckStop.deleteMany();
|
|
await prisma.seasonalRestriction.deleteMany();
|
|
await prisma.contact.deleteMany();
|
|
await prisma.equipmentRequirement.deleteMany();
|
|
await prisma.regulation.deleteMany();
|
|
await prisma.state.deleteMany();
|
|
|
|
// ---------------------------------------------------------------
|
|
// 1. States + Regulations (from MOCK_STATE_REGULATIONS)
|
|
// ---------------------------------------------------------------
|
|
console.log(' 📍 Seeding states and regulations...');
|
|
const stateMap = {}; // abbr -> state.id
|
|
|
|
for (const reg of data1.MOCK_STATE_REGULATIONS) {
|
|
const state = await prisma.state.create({
|
|
data: {
|
|
name: reg.name,
|
|
abbr: reg.abbr,
|
|
lat: reg.lat,
|
|
lng: reg.lng,
|
|
regulation: {
|
|
create: {
|
|
permitWidth: reg.permitWidth,
|
|
permitHeight: reg.permitHeight,
|
|
permitLength: reg.permitLength,
|
|
permitWeight: reg.permitWeight,
|
|
escortWidth: reg.escortWidth,
|
|
escortHeight: reg.escortHeight,
|
|
escortLength: reg.escortLength,
|
|
escortWeight: reg.escortWeight,
|
|
travelRestrictions: reg.travel,
|
|
holidays: reg.holidays,
|
|
agency: reg.agency,
|
|
url: reg.url,
|
|
notes: reg.notes,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
stateMap[reg.abbr] = state.id;
|
|
}
|
|
console.log(` ✅ ${Object.keys(stateMap).length} states with regulations`);
|
|
|
|
// ---------------------------------------------------------------
|
|
// 2. Contacts (from MOCK_STATE_CONTACTS)
|
|
// ---------------------------------------------------------------
|
|
console.log(' 📞 Seeding contacts...');
|
|
let contactCount = 0;
|
|
if (data2.MOCK_STATE_CONTACTS) {
|
|
for (const [abbr, contact] of Object.entries(data2.MOCK_STATE_CONTACTS)) {
|
|
if (!stateMap[abbr]) continue;
|
|
await prisma.contact.create({
|
|
data: {
|
|
stateId: stateMap[abbr],
|
|
permitPhone: contact.permit,
|
|
policePhone: contact.police,
|
|
email: contact.email,
|
|
hours: contact.hours,
|
|
portalUrl: contact.portal,
|
|
},
|
|
});
|
|
contactCount++;
|
|
}
|
|
}
|
|
console.log(` ✅ ${contactCount} state contacts`);
|
|
|
|
// ---------------------------------------------------------------
|
|
// 3. Equipment Requirements (from MOCK_STATE_EQUIPMENT)
|
|
// ---------------------------------------------------------------
|
|
console.log(' 🔧 Seeding equipment requirements...');
|
|
let equipCount = 0;
|
|
if (data2.MOCK_STATE_EQUIPMENT) {
|
|
for (const [abbr, equip] of Object.entries(data2.MOCK_STATE_EQUIPMENT)) {
|
|
if (!stateMap[abbr]) continue;
|
|
|
|
if (equip.escort) {
|
|
await prisma.equipmentRequirement.create({
|
|
data: {
|
|
stateId: stateMap[abbr],
|
|
type: 'escort',
|
|
certification: equip.escort.certification || '',
|
|
vehicle: equip.escort.vehicle || '',
|
|
signs: equip.escort.signs || '',
|
|
lights: equip.escort.lights || '',
|
|
heightPole: equip.escort.heightPole || '',
|
|
flags: equip.escort.flags || '',
|
|
safetyGear: equip.escort.safety || '',
|
|
communication: equip.escort.communication || '',
|
|
},
|
|
});
|
|
equipCount++;
|
|
}
|
|
|
|
if (equip.carrier) {
|
|
await prisma.equipmentRequirement.create({
|
|
data: {
|
|
stateId: stateMap[abbr],
|
|
type: 'carrier',
|
|
signs: equip.carrier.signs || '',
|
|
flags: equip.carrier.flags || '',
|
|
lights: equip.carrier.lights || '',
|
|
safetyGear: [
|
|
equip.carrier.cones ? `Cones: ${equip.carrier.cones}` : '',
|
|
equip.carrier.fireExtinguisher ? `Fire ext: ${equip.carrier.fireExtinguisher}` : '',
|
|
equip.carrier.triangles ? `Triangles: ${equip.carrier.triangles}` : '',
|
|
equip.carrier.flares ? `Flares: ${equip.carrier.flares}` : '',
|
|
equip.carrier.firstAid ? `First aid: ${equip.carrier.firstAid}` : '',
|
|
].filter(Boolean).join('; '),
|
|
},
|
|
});
|
|
equipCount++;
|
|
}
|
|
}
|
|
}
|
|
console.log(` ✅ ${equipCount} equipment requirements`);
|
|
|
|
// ---------------------------------------------------------------
|
|
// 4. Truck Stops (from MOCK_TRUCK_STOPS)
|
|
// ---------------------------------------------------------------
|
|
console.log(' ⛽ Seeding truck stops...');
|
|
let tsCount = 0;
|
|
if (data2.MOCK_TRUCK_STOPS) {
|
|
for (const ts of data2.MOCK_TRUCK_STOPS) {
|
|
const stateAbbr = ts.location?.state;
|
|
const stateId = stateMap[stateAbbr];
|
|
if (!stateId) continue;
|
|
|
|
await prisma.truckStop.create({
|
|
data: {
|
|
stateId,
|
|
name: ts.name,
|
|
lat: ts.location.lat,
|
|
lng: ts.location.lng,
|
|
address: `${ts.location.city}, ${ts.location.state}`,
|
|
hasOversizeParking: ts.oversizeFriendly || false,
|
|
entranceHeight: ts.entranceHeight || '',
|
|
entranceWidth: ts.entranceWidth || '',
|
|
lotSqFt: ts.lotSize ? parseInt(String(ts.lotSize).replace(/[^0-9]/g, '')) || null : null,
|
|
facilities: ts.facilities || [],
|
|
phone: '',
|
|
},
|
|
});
|
|
tsCount++;
|
|
}
|
|
}
|
|
console.log(` ✅ ${tsCount} truck stops`);
|
|
|
|
// ---------------------------------------------------------------
|
|
// 5. Bridge Clearances (from MOCK_BRIDGE_CLEARANCES)
|
|
// ---------------------------------------------------------------
|
|
console.log(' 🌉 Seeding bridges...');
|
|
let bridgeCount = 0;
|
|
if (data2.MOCK_BRIDGE_CLEARANCES) {
|
|
for (const b of data2.MOCK_BRIDGE_CLEARANCES) {
|
|
const stateAbbr = b.location?.state;
|
|
const stateId = stateMap[stateAbbr];
|
|
if (!stateId) continue;
|
|
|
|
await prisma.bridge.create({
|
|
data: {
|
|
stateId,
|
|
name: `${b.type || 'Bridge'} at ${b.location.desc || b.route}`,
|
|
lat: b.location.lat,
|
|
lng: b.location.lng,
|
|
route: b.route,
|
|
heightClearance: parseFloat(String(b.clearanceHeight).replace(/[^0-9.]/g, '')) || 0,
|
|
widthClearance: b.clearanceWidth ? parseFloat(String(b.clearanceWidth).replace(/[^0-9.]/g, '')) : null,
|
|
weightLimit: b.weightLimit ? parseFloat(String(b.weightLimit).replace(/[^0-9.]/g, '')) : null,
|
|
},
|
|
});
|
|
bridgeCount++;
|
|
}
|
|
}
|
|
console.log(` ✅ ${bridgeCount} bridges`);
|
|
|
|
// ---------------------------------------------------------------
|
|
// 6. Weigh Stations (from MOCK_WEIGH_STATIONS)
|
|
// ---------------------------------------------------------------
|
|
console.log(' ⚖️ Seeding weigh stations...');
|
|
let wsCount = 0;
|
|
if (data2.MOCK_WEIGH_STATIONS) {
|
|
for (const ws of data2.MOCK_WEIGH_STATIONS) {
|
|
const stateAbbr = ws.location?.state;
|
|
const stateId = stateMap[stateAbbr];
|
|
if (!stateId) continue;
|
|
|
|
await prisma.weighStation.create({
|
|
data: {
|
|
stateId,
|
|
name: ws.name,
|
|
lat: ws.location.lat,
|
|
lng: ws.location.lng,
|
|
direction: ws.direction || '',
|
|
route: ws.route || '',
|
|
prePass: ws.prePass || false,
|
|
hours: ws.hours || '',
|
|
currentStatus: ws.currentStatus || 'unknown',
|
|
},
|
|
});
|
|
wsCount++;
|
|
}
|
|
}
|
|
console.log(` ✅ ${wsCount} weigh stations`);
|
|
|
|
// ---------------------------------------------------------------
|
|
// 7. Alerts — Route Conditions + Weather (from MOCK_ROUTE_CONDITIONS + MOCK_WEATHER_ALERTS)
|
|
// ---------------------------------------------------------------
|
|
console.log(' 🚨 Seeding alerts...');
|
|
let alertCount = 0;
|
|
|
|
if (data2.MOCK_ROUTE_CONDITIONS) {
|
|
for (const rc of data2.MOCK_ROUTE_CONDITIONS) {
|
|
const stateAbbr = rc.location?.state;
|
|
const stateId = stateMap[stateAbbr];
|
|
if (!stateId) continue;
|
|
|
|
await prisma.alert.create({
|
|
data: {
|
|
stateId,
|
|
type: rc.type || 'construction',
|
|
route: rc.route || '',
|
|
description: rc.description || '',
|
|
severity: rc.severity || 'info',
|
|
startsAt: rc.startDate ? new Date(rc.startDate) : new Date(),
|
|
endsAt: rc.endDate ? new Date(rc.endDate) : null,
|
|
},
|
|
});
|
|
alertCount++;
|
|
}
|
|
}
|
|
|
|
if (data2.MOCK_WEATHER_ALERTS) {
|
|
for (const wa of data2.MOCK_WEATHER_ALERTS) {
|
|
// Weather alerts may not have a state directly, try to match from region
|
|
const stateAbbr = wa.state || (wa.routes?.[0] ? 'TX' : null); // fallback
|
|
const stateId = stateAbbr ? stateMap[stateAbbr] : Object.values(stateMap)[0];
|
|
if (!stateId) continue;
|
|
|
|
await prisma.alert.create({
|
|
data: {
|
|
stateId,
|
|
type: wa.type || 'weather',
|
|
route: (wa.routes || []).join(', '),
|
|
description: wa.description || '',
|
|
severity: wa.severity || 'info',
|
|
startsAt: wa.validFrom ? new Date(wa.validFrom) : new Date(),
|
|
endsAt: wa.validTo ? new Date(wa.validTo) : null,
|
|
},
|
|
});
|
|
alertCount++;
|
|
}
|
|
}
|
|
console.log(` ✅ ${alertCount} alerts`);
|
|
|
|
// ---------------------------------------------------------------
|
|
// 8. Seasonal Restrictions (from MOCK_SEASONAL_RESTRICTIONS)
|
|
// ---------------------------------------------------------------
|
|
console.log(' 📅 Seeding seasonal restrictions...');
|
|
let seasonCount = 0;
|
|
if (data2.MOCK_SEASONAL_RESTRICTIONS) {
|
|
for (const sr of data2.MOCK_SEASONAL_RESTRICTIONS) {
|
|
const stateAbbr = sr.state;
|
|
const stateId = stateMap[stateAbbr];
|
|
if (!stateId) continue;
|
|
|
|
await prisma.seasonalRestriction.create({
|
|
data: {
|
|
stateId,
|
|
name: sr.title || sr.type,
|
|
type: sr.type || 'other',
|
|
startMonth: sr.startMonth || 1,
|
|
endMonth: sr.endMonth || 12,
|
|
description: sr.description || '',
|
|
},
|
|
});
|
|
seasonCount++;
|
|
}
|
|
}
|
|
console.log(` ✅ ${seasonCount} seasonal restrictions`);
|
|
|
|
// ---------------------------------------------------------------
|
|
// Summary
|
|
// ---------------------------------------------------------------
|
|
console.log('\n🎉 Seed complete!\n');
|
|
}
|
|
|
|
seed()
|
|
.catch((err) => {
|
|
console.error('❌ Seed failed:', err);
|
|
process.exit(1);
|
|
})
|
|
.finally(async () => {
|
|
await prisma.$disconnect();
|
|
});
|