Refactor seeding scripts to utilize generateIdWithDbCounter for unique ID generation across crime categories, incidents, and units. Implement a new utility function for generating distributed points within district areas to enhance incident location variability. Comment out unnecessary type drops and creations in SQL migration files for clarity. Add migration scripts to drop and re-add the phone field in the units table, ensuring data integrity during schema updates.

This commit is contained in:
vergiLgood1 2025-05-06 01:21:04 +07:00
parent 0747897fc7
commit e891df87d0
9 changed files with 788 additions and 549 deletions

File diff suppressed because it is too large Load Diff

View File

@ -316,12 +316,12 @@ using ((bucket_id = 'avatars'::text));
drop type "gis"."geometry_dump";
-- drop type "gis"."geometry_dump";
drop type "gis"."valid_detail";
-- drop type "gis"."valid_detail";
create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry);
-- create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry);
create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry);
-- create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry);

View File

@ -0,0 +1,8 @@
/*
Warnings:
- You are about to drop the column `phone` on the `units` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "units" DROP COLUMN "phone";

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "units" ADD COLUMN "phone" TEXT;

View File

@ -1,6 +1,6 @@
// prisma/seeds/CrimeCategoriesSeeder.ts
import { generateId } from "../../app/_utils/common";
import { PrismaClient } from "@prisma/client";
import { generateId, generateIdWithDbCounter } from '../../app/_utils/common';
import { PrismaClient } from '@prisma/client';
import { crimeCategoriesData } from '../data/jsons/crime-category';
import path from 'path';
@ -36,16 +36,14 @@ export class CrimeCategoriesSeeder {
const data = XLSX.utils.sheet_to_json(sheet) as ICrimeCategory[];
for (const category of crimeCategoriesData) {
const newId = await generateId({
const newId = await generateIdWithDbCounter('crime_categories', {
prefix: 'CC',
segments: {
sequentialDigits: 4,
},
randomSequence: false,
uniquenessStrategy: 'counter',
format: '{prefix}-{sequence}',
separator: '-',
tableName: 'crime_categories',
storage: 'database',
uniquenessStrategy: 'counter',
});
await this.prisma.crime_categories.create({

View File

@ -4,7 +4,7 @@ import {
crime_status,
crimes,
} from '@prisma/client';
import { generateId } from '../../app/_utils/common';
import { generateId, generateIdWithDbCounter } from '../../app/_utils/common';
import { createClient } from '../../app/_utils/supabase/client';
import * as fs from 'fs';
import * as path from 'path';
@ -64,6 +64,94 @@ export class CrimeIncidentsSeeder {
});
}
/**
* Generates well-distributed points within a district's area
* @param centerLat - The center latitude of the district
* @param centerLng - The center longitude of the district
* @param landArea - Land area in square km
* @param numPoints - Number of points to generate
* @returns Array of {latitude, longitude, radius} points
*/
private generateDistributedPoints(
centerLat: number,
centerLng: number,
landArea: number,
numPoints: number
): Array<{ latitude: number; longitude: number; radius: number }> {
const points = [];
// Calculate a reasonable radius based on land area
// Using square root of area as an approximation of district "radius"
const areaFactor = Math.sqrt(landArea) / 100;
const baseRadius = Math.max(0.01, Math.min(0.05, areaFactor * 0.03));
// Create a grid-based distribution for better coverage
const gridSize = Math.ceil(Math.sqrt(numPoints * 1.5)); // Slightly larger grid for variety
// Define district bounds approximately
// 0.1 degrees is roughly 11km at equator
const estimatedDistrictRadius = Math.sqrt(landArea / Math.PI) / 111;
const bounds = {
minLat: centerLat - estimatedDistrictRadius,
maxLat: centerLat + estimatedDistrictRadius,
minLng: centerLng - estimatedDistrictRadius,
maxLng: centerLng + estimatedDistrictRadius,
};
const latStep = (bounds.maxLat - bounds.minLat) / gridSize;
const lngStep = (bounds.maxLng - bounds.minLng) / gridSize;
// Generate points in each grid cell with some randomness
let totalPoints = 0;
for (let i = 0; i < gridSize && totalPoints < numPoints; i++) {
for (let j = 0; j < gridSize && totalPoints < numPoints; j++) {
// Base position within the grid cell
const cellLat =
bounds.minLat + (i + 0.2 + Math.random() * 0.6) * latStep;
const cellLng =
bounds.minLng + (j + 0.2 + Math.random() * 0.6) * lngStep;
// Distance from center (for radius reference)
const latDiff = cellLat - centerLat;
const lngDiff = cellLng - centerLng;
const distance = Math.sqrt(latDiff * latDiff + lngDiff * lngDiff);
// Add some randomness to avoid perfect grid pattern
const jitter = baseRadius * 0.2;
const latitude = cellLat + (Math.random() * 2 - 1) * jitter;
const longitude = cellLng + (Math.random() * 2 - 1) * jitter;
points.push({
latitude,
longitude,
radius: distance * 111000, // Convert to meters (approx)
});
totalPoints++;
}
}
// Add some completely random points for diversity
while (points.length < numPoints) {
const latitude =
centerLat + (Math.random() * 2 - 1) * estimatedDistrictRadius;
const longitude =
centerLng + (Math.random() * 2 - 1) * estimatedDistrictRadius;
const latDiff = latitude - centerLat;
const lngDiff = longitude - centerLng;
const distance = Math.sqrt(latDiff * latDiff + lngDiff * lngDiff);
points.push({
latitude,
longitude,
radius: distance * 111000, // Convert to meters (approx)
});
}
return points;
}
async run(): Promise<void> {
console.log('🌱 Seeding crime incidents data...');
@ -161,66 +249,16 @@ export class CrimeIncidentsSeeder {
return [];
}
// Generate multiple coordinates for this district to add more variety
// We'll create a pool of 5-10 potential locations and select from them randomly for each incident
const locationPool = [];
const numLocations = Math.floor(Math.random() * 6) + 5; // 5-10 locations
// Generate a variable number of incidents between 10 and 25 for more variability
const numLocations = Math.floor(Math.random() * 16) + 10; // 10-25 locations
// Scale radius based on district land area if available
// This creates more realistic distribution based on district size
let baseRadius = 0.02; // Default ~2km
if (geo.land_area) {
// Adjust radius based on land area - larger districts get larger radius
// Square root of area provides a reasonable scale factor
const areaFactor = Math.sqrt(geo.land_area) / 100;
baseRadius = Math.max(0.01, Math.min(0.05, areaFactor * 0.03));
}
for (let i = 0; i < numLocations; i++) {
// Create more varied locations by using different radiuses
const radiusVariation = Math.random() * 0.5 + 0.5; // 50% to 150% of base radius
const radius = baseRadius * radiusVariation;
// Different angle for each location
const angle = Math.random() * Math.PI * 2;
// Use different distance distribution patterns
// Some close to center, some at middle distance, some near the edge
let distance;
const patternType = Math.floor(Math.random() * 3);
switch (patternType) {
case 0: // Close to center
distance = Math.random() * 0.4 * radius;
break;
case 1: // Middle range
distance = (0.4 + Math.random() * 0.3) * radius;
break;
case 2: // Edge of district
distance = (0.7 + Math.random() * 0.3) * radius;
break;
}
if (!distance || !angle) {
console.error(
`Invalid distance or angle for location generation, skipping.`
);
continue;
}
// Calculate offset with improved approximation
const latOffset = distance * Math.cos(angle);
const lngOffset = distance * Math.sin(angle);
// Apply offset to base coordinates
const latitude = geo.latitude + latOffset;
const longitude = geo.longitude + lngOffset;
locationPool.push({
latitude,
longitude,
radius: distance * 1000, // Convert to meters for reference
});
}
// Generate distributed locations using our new utility function
const locationPool = this.generateDistributedPoints(
geo.latitude,
geo.longitude,
geo.land_area || 100, // Default to 100 km² if not available
numLocations
);
// List of common street names in Jember with more variety
const jemberStreets = [
@ -409,20 +447,21 @@ export class CrimeIncidentsSeeder {
}
// Generate a unique ID for the incident
const incidentId = await generateId({
prefix: 'CI',
segments: {
codes: [district.cities.id],
sequentialDigits: 4,
year,
const incidentId = await generateIdWithDbCounter(
'crime_incidents',
{
prefix: 'CI',
segments: {
codes: [district.city_id],
sequentialDigits: 4,
year,
},
format: '{prefix}-{codes}-{sequence}-{year}',
separator: '-',
uniquenessStrategy: 'counter',
},
format: '{prefix}-{codes}-{sequence}-{year}',
separator: '-',
randomSequence: false,
uniquenessStrategy: 'counter',
storage: 'database',
tableName: 'crime_incidents',
});
/(\d{4})(?=-\d{4}$)/ // Pattern to extract the 4-digit counter
);
// Determine status based on crime_cleared
// If i < crimesCleared, this incident is resolved, otherwise unresolved

View File

@ -8,7 +8,7 @@ import {
import fs from 'fs';
import path from 'path';
import { parse } from 'csv-parse/sync';
import { generateId } from '../../app/_utils/common';
import { generateId, generateIdWithDbCounter } from '../../app/_utils/common';
export class CrimesSeeder {
constructor(private prisma: PrismaClient) {}
@ -177,20 +177,23 @@ export class CrimesSeeder {
const year = parseInt(record.year);
// Create a unique ID for monthly crime data
const crimeId = await generateId({
prefix: 'CR',
segments: {
codes: [city.id],
sequentialDigits: 4,
year,
const crimeId = await generateIdWithDbCounter(
'crimes',
{
prefix: 'CR',
segments: {
codes: [city.id],
sequentialDigits: 4,
year,
},
format: '{prefix}-{codes}-{sequence}-{year}',
separator: '-',
uniquenessStrategy: 'counter',
},
format: '{prefix}-{codes}-{sequence}-{year}',
separator: '-',
randomSequence: false,
uniquenessStrategy: 'counter',
storage: 'database',
tableName: 'crimes',
});
/(\d{4})(?=-\d{4}$)/ // Pattern to extract the 4-digit counter
);
console.log('Creating crime ID:', crimeId);
await this.prisma.crimes.create({
data: {
@ -259,20 +262,36 @@ export class CrimesSeeder {
}
// Create a unique ID for yearly crime data
const crimeId = await generateId({
prefix: 'CR',
segments: {
codes: [city.id],
sequentialDigits: 4,
year,
// const crimeId = await generateId({
// prefix: 'CR',
// segments: {
// codes: [city.id],
// sequentialDigits: 4,
// year,
// },
// format: '{prefix}-{codes}-{sequence}-{year}',
// separator: '-',
// randomSequence: false,
// uniquenessStrategy: 'counter',
// storage: 'database',
// tableName: 'crimes',
// });
const crimeId = await generateIdWithDbCounter(
'crimes',
{
prefix: 'CR',
segments: {
codes: [city.id],
sequentialDigits: 4,
year,
},
format: '{prefix}-{codes}-{sequence}-{year}',
separator: '-',
uniquenessStrategy: 'counter',
},
format: '{prefix}-{codes}-{sequence}-{year}',
separator: '-',
randomSequence: false,
uniquenessStrategy: 'counter',
storage: 'database',
tableName: 'crimes',
});
/(\d{4})(?=-\d{4}$)/ // Pattern to extract the 4-digit counter
);
await this.prisma.crimes.create({
data: {
@ -337,19 +356,20 @@ export class CrimesSeeder {
}
// Create a unique ID for all-year summary data
const crimeId = await generateId({
prefix: 'CR',
segments: {
codes: [city.id],
sequentialDigits: 4,
const crimeId = await generateIdWithDbCounter(
'crimes',
{
prefix: 'CR',
segments: {
codes: [city.id],
sequentialDigits: 4,
},
format: '{prefix}-{codes}-{sequence}',
separator: '-',
uniquenessStrategy: 'counter',
},
format: '{prefix}-{codes}-{sequence}',
separator: '-',
randomSequence: false,
uniquenessStrategy: 'counter',
storage: 'database',
tableName: 'crimes',
});
/(\d{4})$/ // Pattern to extract the 4-digit counter at the end
);
await this.prisma.crimes.create({
data: {

View File

@ -5,7 +5,7 @@ import * as path from 'path';
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
import { createClient } from '../../app/_utils/supabase/client';
import { generateId } from '../../app/_utils/common';
import { generateId, generateIdWithDbCounter } from '../../app/_utils/common';
// Interface untuk data Excel row
interface ExcelRow {
@ -89,17 +89,14 @@ export class UnitSeeder {
const address = location.address;
const phone = location.telepon?.replace(/-/g, '');
const newId = await generateId({
const newId = await generateIdWithDbCounter('units', {
prefix: 'UT',
format: '{prefix}-{sequence}',
separator: '-',
randomSequence: false,
uniquenessStrategy: 'counter',
segments: {
sequentialDigits: 4,
},
storage: 'database',
tableName: 'units',
format: '{prefix}-{sequence}',
separator: '-',
uniquenessStrategy: 'counter',
});
let locationData: CreateLocationDto = {
@ -154,17 +151,14 @@ export class UnitSeeder {
const address = location.address;
const phone = location.telepon?.replace(/-/g, '');
const newId = await generateId({
const newId = await generateIdWithDbCounter('units', {
prefix: 'UT',
format: '{prefix}-{sequence}',
separator: '-',
randomSequence: false,
uniquenessStrategy: 'counter',
segments: {
sequentialDigits: 4,
},
storage: 'database',
tableName: 'units',
format: '{prefix}-{sequence}',
separator: '-',
uniquenessStrategy: 'counter',
});
const locationData: CreateLocationDto = {

View File

@ -1,9 +1,9 @@
drop type "gis"."geometry_dump";
-- drop type "gis"."geometry_dump";
drop type "gis"."valid_detail";
-- drop type "gis"."valid_detail";
create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry);
-- create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry);
create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry);
-- create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry);