Add Node.js/Express backend with PostgreSQL and wire frontend to API
- 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>
This commit is contained in:
346
server/src/seeds/seed.js
Normal file
346
server/src/seeds/seed.js
Normal file
@@ -0,0 +1,346 @@
|
||||
// =====================================================================
|
||||
// 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();
|
||||
});
|
||||
Reference in New Issue
Block a user