286 lines
9.2 KiB
TypeScript
286 lines
9.2 KiB
TypeScript
import { PrismaClient } from "@prisma/client";
|
|
import { faker } from "@faker-js/faker";
|
|
import { districtCenters } from "../data/jsons/district-center";
|
|
|
|
import { createClient } from "../../app/_utils/supabase/client";
|
|
import db from "../db";
|
|
|
|
export class IncidentLogSeeder {
|
|
constructor(
|
|
private prisma: PrismaClient,
|
|
private supabase = createClient(),
|
|
) {}
|
|
|
|
// Add run method to satisfy the Seeder interface
|
|
async run(): Promise<void> {
|
|
await db.incident_logs.deleteMany({});
|
|
|
|
await this.seed();
|
|
}
|
|
|
|
async seed() {
|
|
// Step 1: Create a mock user if needed
|
|
const user = await this.getOrCreateUser();
|
|
|
|
// Step 2: Create an event
|
|
const event = await this.createEvent(user.id);
|
|
|
|
// Step 3: Create a session
|
|
const session = await this.createSession(user.id, event.id);
|
|
|
|
// Step 4: Create locations
|
|
const locations = await this.createLocations(event.id);
|
|
|
|
// Step 5: Create incident logs for the past 24 hours
|
|
await this.createIncidentLogs(user.id, locations);
|
|
|
|
console.log("Incident logs seeded successfully");
|
|
}
|
|
|
|
private async getOrCreateUser() {
|
|
// Check if we have an existing user
|
|
const existingUser = await this.prisma.users.findFirst({
|
|
where: { email: "incident-reporter@sigap.com" },
|
|
});
|
|
|
|
if (existingUser) return existingUser;
|
|
|
|
// Get a valid role ID
|
|
const role = await this.prisma.roles.findFirst();
|
|
if (!role) {
|
|
throw new Error("No roles found. Please seed roles first.");
|
|
}
|
|
|
|
// Create a new user if none exists
|
|
return await this.prisma.users.create({
|
|
data: {
|
|
email: "incident-reporter@sigap.com",
|
|
roles_id: role.id,
|
|
is_anonymous: false,
|
|
},
|
|
});
|
|
}
|
|
|
|
private async createEvent(userId: string) {
|
|
return await this.prisma.events.create({
|
|
data: {
|
|
name: "Incident Monitoring Event",
|
|
description: "24-hour incident monitoring",
|
|
user_id: userId,
|
|
},
|
|
});
|
|
}
|
|
|
|
private async createSession(userId: string, eventId: string) {
|
|
return await this.prisma.sessions.create({
|
|
data: {
|
|
user_id: userId,
|
|
event_id: eventId,
|
|
status: "active",
|
|
},
|
|
});
|
|
}
|
|
|
|
private async createLocations(eventId: string) {
|
|
const districts = await this.prisma.districts.findMany({});
|
|
|
|
if (!districts.length) {
|
|
throw new Error("No districts found. Please seed districts first.");
|
|
}
|
|
|
|
const locations = [];
|
|
|
|
// Create 10 random locations across the districts
|
|
for (let i = 0; i < 10; i++) {
|
|
const district =
|
|
districts[Math.floor(Math.random() * districts.length)];
|
|
|
|
// Find matching district center by name
|
|
const districtCenter = districtCenters.find(
|
|
(center) =>
|
|
center.kecamatan.toLowerCase() ===
|
|
district.name.toLowerCase(),
|
|
);
|
|
|
|
// If we have matching center coordinates, use them as base point
|
|
// Otherwise generate random coordinates
|
|
let latitude, longitude;
|
|
|
|
if (districtCenter) {
|
|
// Generate random coordinates within 3-5km of district center
|
|
const radius = this.getRandomInt(3000, 5000); // 3-5 km in meters
|
|
const randomPoint = this.getRandomPointInRadius(
|
|
districtCenter.lat,
|
|
districtCenter.lng,
|
|
radius,
|
|
);
|
|
|
|
latitude = randomPoint.latitude;
|
|
longitude = randomPoint.longitude;
|
|
}
|
|
|
|
const locationType = [
|
|
"residential",
|
|
"commercial",
|
|
"public",
|
|
"transportation",
|
|
][Math.floor(Math.random() * 4)];
|
|
const address = faker.location.streetAddress();
|
|
|
|
// Insert using Supabase with PostGIS
|
|
const { data, error } = await this.supabase
|
|
.from("locations")
|
|
.insert({
|
|
district_id: district.id,
|
|
event_id: eventId,
|
|
address: address,
|
|
type: locationType,
|
|
latitude: latitude,
|
|
longitude: longitude,
|
|
location: `POINT(${longitude} ${latitude})`, // PostGIS format
|
|
})
|
|
.select();
|
|
|
|
if (error) {
|
|
console.error("Error inserting location:", error);
|
|
continue;
|
|
}
|
|
|
|
if (data && data.length > 0) {
|
|
locations.push(data[0]);
|
|
}
|
|
}
|
|
|
|
return locations;
|
|
}
|
|
|
|
private async createIncidentLogs(userId: string, locations: any[]) {
|
|
// Get crime categories
|
|
const categories = await this.prisma.crime_categories.findMany({});
|
|
|
|
if (!categories.length) {
|
|
throw new Error(
|
|
"No crime categories found. Please seed crime categories first.",
|
|
);
|
|
}
|
|
|
|
const now = new Date();
|
|
|
|
// Create 24 incidents data array
|
|
const incidentData = [];
|
|
|
|
for (let i = 0; i < 24; i++) {
|
|
const hourOffset = this.getRandomInt(0, 24); // Random hour within last 24 hours
|
|
const timestamp = new Date(
|
|
now.getTime() - hourOffset * 60 * 60 * 1000,
|
|
);
|
|
|
|
const location =
|
|
locations[Math.floor(Math.random() * locations.length)];
|
|
const category =
|
|
categories[Math.floor(Math.random() * categories.length)];
|
|
|
|
incidentData.push({
|
|
user_id: userId,
|
|
location_id: location.id,
|
|
category_id: category.id,
|
|
description: this.getRandomIncidentDescription(),
|
|
time: timestamp,
|
|
source: Math.random() > 0.3 ? "resident" : "reporter",
|
|
verified: Math.random() > 0.5,
|
|
});
|
|
}
|
|
|
|
// Bulk insert all incidents at once
|
|
const createdIncidents = await this.prisma.incident_logs.createMany({
|
|
data: incidentData,
|
|
});
|
|
|
|
console.log(`Created ${createdIncidents.count} incident logs in bulk`);
|
|
|
|
// If you need the actual created records, query them after creation
|
|
const incidents = await this.prisma.incident_logs.findMany({
|
|
where: {
|
|
user_id: userId,
|
|
time: {
|
|
gte: new Date(now.getTime() - 24 * 60 * 60 * 1000),
|
|
},
|
|
},
|
|
orderBy: {
|
|
time: "asc",
|
|
},
|
|
});
|
|
|
|
return incidents;
|
|
}
|
|
|
|
// Helper methods
|
|
private getRandomInt(min: number, max: number): number {
|
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
}
|
|
|
|
private getRandomIncidentDescription(): string {
|
|
const descriptions = [
|
|
"Suspicious person loitering in the area",
|
|
"Vehicle break-in reported",
|
|
"Shoplifting incident at local store",
|
|
"Noise complaint from neighbors",
|
|
"Traffic accident with minor injuries",
|
|
"Vandalism to public property",
|
|
"Domestic dispute reported",
|
|
"Trespassing on private property",
|
|
"Armed robbery at convenience store",
|
|
"Drug-related activity observed",
|
|
"Assault reported outside nightclub",
|
|
"Missing person report filed",
|
|
"Public intoxication incident",
|
|
"Package theft from doorstep",
|
|
"Illegal dumping observed",
|
|
];
|
|
|
|
return descriptions[Math.floor(Math.random() * descriptions.length)];
|
|
}
|
|
|
|
/**
|
|
* Generates a random point within a specified radius from a center point
|
|
* @param centerLat Center latitude
|
|
* @param centerLng Center longitude
|
|
* @param radiusInMeters Radius in meters
|
|
* @returns Object containing latitude and longitude
|
|
*/
|
|
private getRandomPointInRadius(
|
|
centerLat: number,
|
|
centerLng: number,
|
|
radiusInMeters: number,
|
|
): { latitude: number; longitude: number } {
|
|
// Earth's radius in meters
|
|
const earthRadius = 6378137;
|
|
|
|
// Convert radius from meters to degrees
|
|
const radiusInDegrees = radiusInMeters / earthRadius * (180 / Math.PI);
|
|
|
|
// Generate random angle in radians
|
|
const randomAngle = Math.random() * Math.PI * 2;
|
|
|
|
// Generate random radius within the specified radius
|
|
const randomRadius = Math.sqrt(Math.random()) * radiusInDegrees;
|
|
|
|
// Calculate offset
|
|
const latOffset = randomRadius * Math.sin(randomAngle);
|
|
const lngOffset = randomRadius * Math.cos(randomAngle);
|
|
|
|
// Adjust for earth's curvature for longitude
|
|
const lngOffsetAdjusted = lngOffset /
|
|
Math.cos(centerLat * Math.PI / 180);
|
|
|
|
// Calculate final coordinates
|
|
const randomLat = centerLat + latOffset;
|
|
const randomLng = centerLng + lngOffsetAdjusted;
|
|
|
|
return {
|
|
latitude: randomLat,
|
|
longitude: randomLng,
|
|
};
|
|
}
|
|
}
|