Refactor GeoJSON seeder to insert data in smaller batches to prevent timeouts; added chunking logic and improved error handling. Update permission seeder to batch create permissions for roles and resources. Optimize unit seeder to prepare and insert unit data in bulk, enhancing performance. Adjust Supabase configuration for local development and modify migration scripts to restore GIS functions and types.
This commit is contained in:
parent
969d10958c
commit
57fb1e4e46
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["denoland.vscode-deno"]
|
||||
}
|
|
@ -1,3 +1,25 @@
|
|||
{
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "denoland.vscode-deno"
|
||||
},
|
||||
"deno.enablePaths": [
|
||||
"supabase/functions"
|
||||
],
|
||||
"deno.lint": true,
|
||||
"deno.unstable": [
|
||||
"bare-node-builtins",
|
||||
"byonm",
|
||||
"sloppy-imports",
|
||||
"unsafe-proto",
|
||||
"webgpu",
|
||||
"broadcast-channel",
|
||||
"worker-options",
|
||||
"cron",
|
||||
"kv",
|
||||
"ffi",
|
||||
"fs",
|
||||
"http",
|
||||
"net"
|
||||
],
|
||||
"files.autoSave": "off"
|
||||
}
|
||||
|
|
|
@ -263,7 +263,6 @@ model incident_logs {
|
|||
model units {
|
||||
code_unit String @id @unique @db.VarChar(20)
|
||||
district_id String @db.VarChar(20)
|
||||
city_id String @db.VarChar(20)
|
||||
name String @db.VarChar(100)
|
||||
description String?
|
||||
type unit_type
|
||||
|
@ -275,9 +274,10 @@ model units {
|
|||
longitude Float
|
||||
location Unsupported("geography")
|
||||
phone String?
|
||||
city_id String @db.VarChar(20)
|
||||
unit_statistics unit_statistics[]
|
||||
districts districts @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
cities cities @relation(fields: [city_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
districts districts @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
|
||||
@@index([name], map: "idx_units_name")
|
||||
@@index([type], map: "idx_units_type")
|
||||
|
|
|
@ -35,6 +35,10 @@ export class CrimeCategoriesSeeder {
|
|||
const sheet = workbook.Sheets[workbook.SheetNames[0]];
|
||||
const data = XLSX.utils.sheet_to_json(sheet) as ICrimeCategory[];
|
||||
|
||||
// Prepare array for batch insertion
|
||||
const categoriesToCreate = [];
|
||||
|
||||
// Generate IDs and prepare data for batch insertion
|
||||
for (const category of crimeCategoriesData) {
|
||||
const newId = await generateIdWithDbCounter('crime_categories', {
|
||||
prefix: 'CC',
|
||||
|
@ -46,32 +50,41 @@ export class CrimeCategoriesSeeder {
|
|||
uniquenessStrategy: 'counter',
|
||||
});
|
||||
|
||||
await this.prisma.crime_categories.create({
|
||||
data: {
|
||||
id: newId.trim(),
|
||||
name: category.name,
|
||||
description: category.description,
|
||||
},
|
||||
categoriesToCreate.push({
|
||||
id: newId.trim(),
|
||||
name: category.name,
|
||||
description: category.description,
|
||||
});
|
||||
|
||||
console.log(`Seeding crime category: ${category.name}`);
|
||||
}
|
||||
|
||||
// Update existing records
|
||||
for (const row of data) {
|
||||
const id = row['id'].trim();
|
||||
const name = row['name'].trim();
|
||||
const type = row['type'].trim();
|
||||
// Batch create categories
|
||||
await this.prisma.crime_categories.createMany({
|
||||
data: categoriesToCreate,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
await this.prisma.crime_categories.updateMany({
|
||||
where: { id },
|
||||
data: {
|
||||
type,
|
||||
},
|
||||
});
|
||||
console.log(`Batch created ${categoriesToCreate.length} crime categories.`);
|
||||
|
||||
console.log(`Updating crime category: ${name} to type ${type}`);
|
||||
}
|
||||
// Prepare data for batch update
|
||||
const categoriesToUpdate = data.map((row) => ({
|
||||
id: row['id'].trim(),
|
||||
type: row['type'].trim(),
|
||||
name: row['name'].trim(),
|
||||
}));
|
||||
|
||||
// Batch update is not directly supported by Prisma, so we'll use Promise.all with individual updates
|
||||
await Promise.all(
|
||||
categoriesToUpdate.map((category) =>
|
||||
this.prisma.crime_categories.updateMany({
|
||||
where: { id: category.id },
|
||||
data: { type: category.type },
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Updated types for ${categoriesToUpdate.length} crime categories.`
|
||||
);
|
||||
|
||||
console.log(`✅ ${crimeCategoriesData.length} crime categories seeded`);
|
||||
}
|
||||
|
|
|
@ -212,6 +212,34 @@ export class CrimeIncidentsSeeder {
|
|||
return points;
|
||||
}
|
||||
|
||||
// Helper for chunked insertion
|
||||
private async chunkedInsertIncidents(data: any[], chunkSize: number = 200) {
|
||||
for (let i = 0; i < data.length; i += chunkSize) {
|
||||
const chunk = data.slice(i, i + chunkSize);
|
||||
await this.prisma.crime_incidents.createMany({
|
||||
data: chunk,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for chunked Supabase insert
|
||||
private async chunkedInsertLocations(
|
||||
locations: any[],
|
||||
chunkSize: number = 200
|
||||
) {
|
||||
for (let i = 0; i < locations.length; i += chunkSize) {
|
||||
const chunk = locations.slice(i, i + chunkSize);
|
||||
let { error } = await this.supabase
|
||||
.from('locations')
|
||||
.insert(chunk)
|
||||
.select();
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
console.log('🌱 Seeding crime incidents data...');
|
||||
|
||||
|
@ -372,7 +400,7 @@ export class CrimeIncidentsSeeder {
|
|||
|
||||
const user = await this.prisma.users.findFirst({
|
||||
where: {
|
||||
email: 'admin@gmail.com',
|
||||
email: 'sigapcompany@gmail.com',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
|
@ -414,6 +442,9 @@ export class CrimeIncidentsSeeder {
|
|||
);
|
||||
}
|
||||
|
||||
const incidentsToCreate = [];
|
||||
const locationsToCreate = [];
|
||||
|
||||
// Create incidents based on the number_of_crime value
|
||||
for (let i = 0; i < crime.number_of_crime; i++) {
|
||||
// Select random category
|
||||
|
@ -476,39 +507,9 @@ export class CrimeIncidentsSeeder {
|
|||
location: `POINT(${selectedLocation.longitude} ${selectedLocation.latitude})`,
|
||||
};
|
||||
|
||||
let { data: newLocation, error } = await this.supabase
|
||||
.from('locations')
|
||||
.insert([locationData])
|
||||
.select();
|
||||
// Tambahkan ke array, bukan langsung create ke database
|
||||
locationsToCreate.push(locationData);
|
||||
|
||||
if (error) {
|
||||
console.error(
|
||||
`Error inserting into locations for district ${district.name} (${crime.year}):`,
|
||||
error
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const location = await this.prisma.locations.findFirst({
|
||||
where: {
|
||||
event_id: event.id,
|
||||
district_id: district.id,
|
||||
address: randomAddress,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
address: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!location) {
|
||||
console.error(
|
||||
`Location not found for district ${district.name} (${crime.year}), skipping.`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Generate a unique ID for the incident
|
||||
const incidentId = await generateIdWithDbCounter(
|
||||
'crime_incidents',
|
||||
{
|
||||
|
@ -527,7 +528,10 @@ export class CrimeIncidentsSeeder {
|
|||
|
||||
// Determine status based on crime_cleared
|
||||
// If i < crimesCleared, this incident is resolved, otherwise unresolved
|
||||
const status = i < crimesCleared ? 'resolved' : 'unresolved';
|
||||
const status =
|
||||
i < crimesCleared
|
||||
? ('resolved' as crime_status)
|
||||
: ('unresolved' as crime_status);
|
||||
|
||||
// More detailed location descriptions
|
||||
const locs = [
|
||||
|
@ -551,13 +555,13 @@ export class CrimeIncidentsSeeder {
|
|||
const randomLocation = locs[Math.floor(Math.random() * locs.length)];
|
||||
|
||||
const descriptions = [
|
||||
`Kasus ${randomCategory.name.toLowerCase()} ${location.address}`,
|
||||
`Kasus ${randomCategory.name.toLowerCase()} ${randomAddress}`,
|
||||
`Laporan ${randomCategory.name.toLowerCase()} terjadi pada ${timestamp} ${randomLocation}`,
|
||||
`${randomCategory.name} dilaporkan ${randomLocation}`,
|
||||
`Insiden ${randomCategory.name.toLowerCase()} terjadi ${randomLocation}`,
|
||||
`Kejadian ${randomCategory.name.toLowerCase()} ${randomLocation}`,
|
||||
`${randomCategory.name} terdeteksi ${randomLocation} pada ${timestamp.toLocaleTimeString()}`,
|
||||
`Pelaporan ${randomCategory.name.toLowerCase()} di ${location.address}`,
|
||||
`Pelaporan ${randomCategory.name.toLowerCase()} di ${randomAddress}`,
|
||||
`Kasus ${randomCategory.name.toLowerCase()} terjadi di ${streetName}`,
|
||||
`${randomCategory.name} terjadi di dekat ${placeType.toLowerCase()} ${district.name}`,
|
||||
`Insiden ${randomCategory.name.toLowerCase()} dilaporkan warga setempat ${randomLocation}`,
|
||||
|
@ -566,23 +570,67 @@ export class CrimeIncidentsSeeder {
|
|||
const randomDescription =
|
||||
descriptions[Math.floor(Math.random() * descriptions.length)];
|
||||
|
||||
// Create the crime incident
|
||||
const incident = await this.prisma.crime_incidents.create({
|
||||
data: {
|
||||
id: incidentId,
|
||||
crime_id: crime.id,
|
||||
crime_category_id: randomCategory.id,
|
||||
location_id: location.id,
|
||||
description: randomDescription,
|
||||
victim_count: 0,
|
||||
status: status,
|
||||
timestamp: timestamp,
|
||||
},
|
||||
incidentsToCreate.push({
|
||||
id: incidentId,
|
||||
crime_id: crime.id,
|
||||
crime_category_id: randomCategory.id,
|
||||
location_id: undefined as string | undefined, // This will be updated after locations are created
|
||||
description: randomDescription,
|
||||
victim_count: 0,
|
||||
status: status,
|
||||
timestamp: timestamp,
|
||||
});
|
||||
|
||||
incidentsCreated.push(incident);
|
||||
}
|
||||
|
||||
// Batch insert locations in chunks
|
||||
try {
|
||||
await this.chunkedInsertLocations(locationsToCreate);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error inserting into locations for district ${district.name} (${crime.year}):`,
|
||||
error
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Fetch all created locations for this batch
|
||||
const createdLocations = await this.prisma.locations.findMany({
|
||||
where: {
|
||||
event_id: event.id,
|
||||
district_id: district.id,
|
||||
address: {
|
||||
in: locationsToCreate
|
||||
.map((loc) => loc.address)
|
||||
.filter((address): address is string => address !== undefined),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
address: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Map addresses to location IDs
|
||||
const addressToId = new Map<string, string>();
|
||||
for (const loc of createdLocations) {
|
||||
if (loc.address !== null) {
|
||||
addressToId.set(loc.address, loc.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Assign location_id to each incident
|
||||
for (let i = 0; i < incidentsToCreate.length; i++) {
|
||||
const address = locationsToCreate[i].address;
|
||||
if (typeof address === 'string') {
|
||||
incidentsToCreate[i].location_id = addressToId.get(address);
|
||||
}
|
||||
}
|
||||
|
||||
// Batch insert incidents in chunks
|
||||
await this.chunkedInsertIncidents(incidentsToCreate);
|
||||
|
||||
incidentsCreated.push(...incidentsToCreate);
|
||||
|
||||
return incidentsCreated;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,22 @@ import path from 'path';
|
|||
import { parse } from 'csv-parse/sync';
|
||||
import { generateId, generateIdWithDbCounter } from '../../app/_utils/common';
|
||||
|
||||
interface ICreateUser {
|
||||
id: string;
|
||||
email: string;
|
||||
roles_id: string;
|
||||
confirmed_at: Date | null;
|
||||
email_confirmed_at: Date | null;
|
||||
last_sign_in_at: Date | null;
|
||||
phone: string | null;
|
||||
updated_at: Date | null;
|
||||
created_at: Date | null;
|
||||
app_metadata: any;
|
||||
invited_at: Date | null;
|
||||
user_metadata: any;
|
||||
is_anonymous: boolean;
|
||||
}
|
||||
|
||||
export class CrimesSeeder {
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
|
||||
|
@ -22,6 +38,10 @@ export class CrimesSeeder {
|
|||
// Create test user
|
||||
const user = await this.createUsers();
|
||||
|
||||
if (!user) {
|
||||
throw new Error('Failed to create user');
|
||||
}
|
||||
|
||||
// Create 5 events
|
||||
const events = await this.createEvents(user);
|
||||
|
||||
|
@ -47,7 +67,7 @@ export class CrimesSeeder {
|
|||
private async createUsers() {
|
||||
// Check if test users already exist
|
||||
const existingUser = await this.prisma.users.findFirst({
|
||||
where: { email: 'admin@sigap.id' },
|
||||
where: { email: 'sigapcompany@gmail.com' },
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
|
@ -69,28 +89,35 @@ export class CrimesSeeder {
|
|||
});
|
||||
}
|
||||
|
||||
// Create test users
|
||||
const user = await this.prisma.users.create({
|
||||
// Create test user directly with Prisma (no Supabase)
|
||||
const newUser = await this.prisma.users.create({
|
||||
data: {
|
||||
email: `admin@gmail.com`,
|
||||
email: 'sigapcompany@gmail.com',
|
||||
roles_id: roleId.id,
|
||||
confirmed_at: new Date(),
|
||||
email_confirmed_at: new Date(),
|
||||
last_sign_in_at: null,
|
||||
phone: null,
|
||||
updated_at: new Date(),
|
||||
created_at: new Date(),
|
||||
app_metadata: {},
|
||||
invited_at: null,
|
||||
user_metadata: {},
|
||||
is_anonymous: false,
|
||||
profile: {
|
||||
create: {
|
||||
first_name: 'Admin',
|
||||
last_name: 'Sigap',
|
||||
username: 'admin',
|
||||
username: 'adminsigap',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return user;
|
||||
return newUser;
|
||||
}
|
||||
|
||||
private async createEvents(user: users) {
|
||||
private async createEvents(user: ICreateUser) {
|
||||
// Check if events already exist
|
||||
const existingEvent = await this.prisma.events.findFirst({
|
||||
where: {
|
||||
|
@ -114,7 +141,7 @@ export class CrimesSeeder {
|
|||
return event;
|
||||
}
|
||||
|
||||
private async createSessions(user: users, events: events) {
|
||||
private async createSessions(user: ICreateUser, events: events) {
|
||||
// Check if sessions already exist
|
||||
const existingSession = await this.prisma.sessions.findFirst();
|
||||
|
||||
|
@ -134,6 +161,16 @@ export class CrimesSeeder {
|
|||
return newSession;
|
||||
}
|
||||
|
||||
// Helper function for chunked insertion (with default chunk size 500)
|
||||
private async chunkedCreateMany(data: any[], chunkSize: number = 500) {
|
||||
for (let i = 0; i < data.length; i += chunkSize) {
|
||||
const chunk = data.slice(i, i + chunkSize);
|
||||
await this.prisma.crimes.createMany({
|
||||
data: chunk,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async importMonthlyCrimeData() {
|
||||
console.log('Importing monthly crime data...');
|
||||
|
||||
|
@ -160,7 +197,10 @@ export class CrimesSeeder {
|
|||
// Store unique district IDs to avoid duplicates
|
||||
const processedDistricts = new Set<string>();
|
||||
|
||||
// Import records
|
||||
// Prepare batch data
|
||||
const crimesData = [];
|
||||
|
||||
// Process records
|
||||
for (const record of records) {
|
||||
const crimeRate = record.level.toLowerCase() as crime_rates;
|
||||
|
||||
|
@ -193,25 +233,26 @@ export class CrimesSeeder {
|
|||
/(\d{4})(?=-\d{4}$)/ // Pattern to extract the 4-digit counter
|
||||
);
|
||||
|
||||
console.log('Creating crime ID:', crimeId);
|
||||
// console.log('Creating crime ID:', crimeId);
|
||||
|
||||
await this.prisma.crimes.create({
|
||||
data: {
|
||||
id: crimeId,
|
||||
district_id: record.district_id,
|
||||
level: crimeRate,
|
||||
method: record.method || null,
|
||||
month: parseInt(record.month_num),
|
||||
year: parseInt(record.year),
|
||||
number_of_crime: parseInt(record.number_of_crime),
|
||||
score: parseFloat(record.score),
|
||||
},
|
||||
crimesData.push({
|
||||
id: crimeId,
|
||||
district_id: record.district_id,
|
||||
level: crimeRate,
|
||||
method: record.method || null,
|
||||
month: parseInt(record.month_num),
|
||||
year: parseInt(record.year),
|
||||
number_of_crime: parseInt(record.number_of_crime),
|
||||
score: parseFloat(record.score),
|
||||
});
|
||||
|
||||
// Keep track of unique districts for later creation of crime incidents
|
||||
processedDistricts.add(record.district_id);
|
||||
}
|
||||
|
||||
// Batch create all crimes in chunks
|
||||
await this.chunkedCreateMany(crimesData);
|
||||
|
||||
console.log(`Imported ${records.length} monthly crime records.`);
|
||||
}
|
||||
|
||||
|
@ -241,7 +282,10 @@ export class CrimesSeeder {
|
|||
skip_empty_lines: true,
|
||||
});
|
||||
|
||||
// Import records
|
||||
// Prepare batch data
|
||||
const crimesData = [];
|
||||
|
||||
// Process records
|
||||
for (const record of records) {
|
||||
const crimeRate = record.level.toLowerCase() as crime_rates;
|
||||
const year = parseInt(record.year);
|
||||
|
@ -261,22 +305,6 @@ export class CrimesSeeder {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Create a unique ID for yearly crime data
|
||||
// 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',
|
||||
{
|
||||
|
@ -293,20 +321,21 @@ export class CrimesSeeder {
|
|||
/(\d{4})(?=-\d{4}$)/ // Pattern to extract the 4-digit counter
|
||||
);
|
||||
|
||||
await this.prisma.crimes.create({
|
||||
data: {
|
||||
id: crimeId,
|
||||
district_id: record.district_id,
|
||||
level: crimeRate,
|
||||
method: record.method || 'kmeans',
|
||||
month: null,
|
||||
year: year,
|
||||
number_of_crime: parseInt(record.number_of_crime),
|
||||
score: parseInt(record.score),
|
||||
},
|
||||
crimesData.push({
|
||||
id: crimeId,
|
||||
district_id: record.district_id,
|
||||
level: crimeRate,
|
||||
method: record.method || 'kmeans',
|
||||
month: null,
|
||||
year: year,
|
||||
number_of_crime: parseInt(record.number_of_crime),
|
||||
score: parseInt(record.score),
|
||||
});
|
||||
}
|
||||
|
||||
// Batch create all yearly crimes in chunks
|
||||
await this.chunkedCreateMany(crimesData);
|
||||
|
||||
console.log(`Imported ${records.length} yearly crime records.`);
|
||||
}
|
||||
|
||||
|
@ -336,6 +365,9 @@ export class CrimesSeeder {
|
|||
skip_empty_lines: true,
|
||||
});
|
||||
|
||||
// Prepare batch data
|
||||
const crimesData = [];
|
||||
|
||||
for (const record of records) {
|
||||
const crimeRate = record.level.toLowerCase() as crime_rates;
|
||||
const districtId = record.district_id;
|
||||
|
@ -371,20 +403,21 @@ export class CrimesSeeder {
|
|||
/(\d{4})$/ // Pattern to extract the 4-digit counter at the end
|
||||
);
|
||||
|
||||
await this.prisma.crimes.create({
|
||||
data: {
|
||||
id: crimeId,
|
||||
district_id: districtId,
|
||||
level: crimeRate,
|
||||
method: 'kmeans',
|
||||
month: null,
|
||||
year: null,
|
||||
number_of_crime: parseInt(record.crime_total),
|
||||
score: parseFloat(record.avg_score),
|
||||
},
|
||||
crimesData.push({
|
||||
id: crimeId,
|
||||
district_id: districtId,
|
||||
level: crimeRate,
|
||||
method: 'kmeans',
|
||||
month: null,
|
||||
year: null,
|
||||
number_of_crime: parseInt(record.crime_total),
|
||||
score: parseFloat(record.avg_score),
|
||||
});
|
||||
}
|
||||
|
||||
// Batch create all all-year summaries in chunks
|
||||
await this.chunkedCreateMany(crimesData);
|
||||
|
||||
console.log(`Imported ${records.length} all-year crime summaries.`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ export class DemographicsSeeder {
|
|||
await this.prisma.demographics.deleteMany({});
|
||||
|
||||
const districts = await this.prisma.districts.findMany();
|
||||
// await this.prisma.$executeRaw`TRUNCATE TABLE "demographics" CASCADE`;
|
||||
|
||||
// Load Excel
|
||||
const filePath = path.join(
|
||||
|
@ -27,6 +26,12 @@ export class DemographicsSeeder {
|
|||
|
||||
let counter = 0;
|
||||
|
||||
// Get all district land areas in a single query at the beginning
|
||||
const districtLandAreas = await this.getAllDistrictLandAreas();
|
||||
|
||||
// Collect demographic data to be inserted in batch
|
||||
const demographicsToInsert = [];
|
||||
|
||||
for (const row of data) {
|
||||
const districtName = String(row['Kecamatan']).trim();
|
||||
const year = Number(row['Tahun']);
|
||||
|
@ -42,28 +47,29 @@ export class DemographicsSeeder {
|
|||
continue;
|
||||
}
|
||||
|
||||
const districtLandArea = await this.getDistrictLandArea(district.id);
|
||||
const districtLandArea = districtLandAreas[district.id] || 0;
|
||||
|
||||
const populationDensity =
|
||||
districtLandArea > 0 ? population / districtLandArea : 0;
|
||||
|
||||
await this.prisma.demographics.create({
|
||||
data: {
|
||||
district_id: district.id,
|
||||
year,
|
||||
population,
|
||||
population_density: populationDensity,
|
||||
number_of_unemployed: unemployed,
|
||||
},
|
||||
demographicsToInsert.push({
|
||||
district_id: district.id,
|
||||
year,
|
||||
population,
|
||||
population_density: populationDensity,
|
||||
number_of_unemployed: unemployed,
|
||||
});
|
||||
|
||||
counter++;
|
||||
|
||||
console.log(
|
||||
`Seeding demographic data for district: ${districtName}, year: ${year}`
|
||||
);
|
||||
}
|
||||
|
||||
// Insert all demographic data at once
|
||||
await this.prisma.demographics.createMany({
|
||||
data: demographicsToInsert,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
console.log(`✅ ${counter} demographic records prepared for batch insertion`);
|
||||
console.log(`✅ ${counter} demographic records seeded from Excel`);
|
||||
}
|
||||
|
||||
|
@ -71,20 +77,21 @@ export class DemographicsSeeder {
|
|||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
private async getDistrictLandArea(districtId: string): Promise<number> {
|
||||
const geo = await this.prisma.geographics.findFirst({
|
||||
where: { district_id: districtId },
|
||||
// Get all district land areas at once to avoid multiple database queries
|
||||
private async getAllDistrictLandAreas(): Promise<Record<string, number>> {
|
||||
const geoData = await this.prisma.geographics.findMany({
|
||||
select: {
|
||||
district_id: true,
|
||||
land_area: true,
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
if (!geo) {
|
||||
console.error(`⚠️ Land area not found for district ID: ${districtId}`);
|
||||
return 0;
|
||||
}
|
||||
const landAreas: Record<string, number> = {};
|
||||
geoData.forEach(geo => {
|
||||
landAreas[geo.district_id] = geo.land_area || 0;
|
||||
});
|
||||
|
||||
return geo.land_area || 0;
|
||||
return landAreas;
|
||||
}
|
||||
|
||||
private async getCityLandArea(): Promise<number> {
|
||||
|
|
|
@ -29,6 +29,7 @@ interface DistrictAreaData {
|
|||
export class GeoJSONSeeder {
|
||||
private mapboxToken: string;
|
||||
private areaData: DistrictAreaData = {};
|
||||
private BATCH_SIZE = 20; // Set a smaller batch size to prevent timeout
|
||||
|
||||
constructor(
|
||||
private prisma: PrismaClient,
|
||||
|
@ -102,6 +103,61 @@ export class GeoJSONSeeder {
|
|||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split array into chunks of the specified size
|
||||
*/
|
||||
private chunkArray<T>(array: T[], chunkSize: number): T[][] {
|
||||
const chunks = [];
|
||||
for (let i = 0; i < array.length; i += chunkSize) {
|
||||
chunks.push(array.slice(i, i + chunkSize));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert data in smaller batches to avoid timeout
|
||||
*/
|
||||
private async insertInBatches(data: any[]): Promise<void> {
|
||||
const batches = this.chunkArray(data, this.BATCH_SIZE);
|
||||
|
||||
console.log(
|
||||
`Splitting ${data.length} records into ${batches.length} batches of max ${this.BATCH_SIZE} records`
|
||||
);
|
||||
|
||||
for (let i = 0; i < batches.length; i++) {
|
||||
const batch = batches[i];
|
||||
console.log(
|
||||
`Processing batch ${i + 1}/${batches.length} (${batch.length} records)`
|
||||
);
|
||||
|
||||
const { error } = await this.supabase
|
||||
.from('geographics')
|
||||
.insert(batch)
|
||||
.select();
|
||||
|
||||
if (error) {
|
||||
console.error(`Error inserting batch ${i + 1}:`, error);
|
||||
// Optionally reduce batch size and retry for this specific batch
|
||||
if (batch.length > 5) {
|
||||
console.log(`Retrying batch ${i + 1} with smaller chunks...`);
|
||||
await this.insertInBatches(batch); // Recursive retry with automatic smaller chunks
|
||||
} else {
|
||||
console.error(
|
||||
`Failed to insert items even with small batch size:`,
|
||||
batch
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`Successfully inserted batch ${i + 1} (${batch.length} records)`
|
||||
);
|
||||
}
|
||||
|
||||
// Add a small delay between batches to reduce database load
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
console.log('Seeding GeoJSON data...');
|
||||
|
||||
|
@ -124,7 +180,6 @@ export class GeoJSONSeeder {
|
|||
|
||||
for (const feature of regencyGeoJson.features) {
|
||||
const properties = feature.properties;
|
||||
const geometry = feature.geometry;
|
||||
|
||||
// Cleanup code
|
||||
const regencyCode = properties.kode_kk.replace(/\./g, '').trim();
|
||||
|
@ -137,41 +192,67 @@ export class GeoJSONSeeder {
|
|||
},
|
||||
});
|
||||
|
||||
// 2. Loop Semua District di GeoJSON
|
||||
for (const feature of districtGeoJson.features) {
|
||||
// Prepare arrays for batch operations
|
||||
const districtsToCreate = [];
|
||||
const geographicsToCreate = [];
|
||||
const addressPromises = [];
|
||||
const years = [2020, 2021, 2022, 2023, 2024];
|
||||
|
||||
// 2. Process all districts first to prepare data
|
||||
for (let i = 0; i < districtGeoJson.features.length; i++) {
|
||||
const feature = districtGeoJson.features[i];
|
||||
const properties = feature.properties;
|
||||
const geometry = feature.geometry;
|
||||
|
||||
// Cleanup code
|
||||
const districtCode = properties.kode_kec.replace(/\./g, '').trim();
|
||||
|
||||
// Insert District
|
||||
const district = await this.prisma.districts.create({
|
||||
data: {
|
||||
id: districtCode,
|
||||
name: properties.kecamatan,
|
||||
city_id: regency.id,
|
||||
},
|
||||
// Add to districts batch
|
||||
districtsToCreate.push({
|
||||
id: districtCode,
|
||||
name: properties.kecamatan,
|
||||
city_id: regency.id,
|
||||
});
|
||||
|
||||
console.log(`Inserted district: ${district.name}`);
|
||||
|
||||
// Buat Location satu kali untuk district (tidak dalam loop)
|
||||
// Calculate centroid for each district
|
||||
const centroid = turf.centroid(feature);
|
||||
const [longitude, latitude] = centroid.geometry.coordinates;
|
||||
|
||||
const address = await this.getStreetFromMapbox(longitude, latitude);
|
||||
// Create address lookup promise for this district
|
||||
addressPromises.push(this.getStreetFromMapbox(longitude, latitude));
|
||||
}
|
||||
|
||||
// Insert locations for each year with appropriate land area
|
||||
const years = [2020, 2021, 2022, 2023, 2024];
|
||||
// 3. Insert all districts at once
|
||||
await this.prisma.districts.createMany({
|
||||
data: districtsToCreate,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
console.log(`Inserted ${districtsToCreate.length} districts in batch`);
|
||||
|
||||
// 4. Get all addresses in parallel
|
||||
const addresses = await Promise.all(addressPromises);
|
||||
|
||||
// 5. Prepare geographic data for batch insertion
|
||||
for (let i = 0; i < districtGeoJson.features.length; i++) {
|
||||
const feature = districtGeoJson.features[i];
|
||||
const properties = feature.properties;
|
||||
const geometry = feature.geometry;
|
||||
const districtCode = properties.kode_kec.replace(/\./g, '').trim();
|
||||
const districtName = properties.kecamatan;
|
||||
const address = addresses[i];
|
||||
|
||||
// Calculate centroid
|
||||
const centroid = turf.centroid(feature);
|
||||
const [longitude, latitude] = centroid.geometry.coordinates;
|
||||
|
||||
// Create geographic entries for each year
|
||||
for (const year of years) {
|
||||
const landArea = this.getDistrictArea(district.name, year);
|
||||
const landArea = this.getDistrictArea(districtName, year);
|
||||
|
||||
// Create location data for this district and year
|
||||
const locationData: CreateLocationDto = {
|
||||
district_id: district.id,
|
||||
description: `Location for ${district.name} District in Jember (${year})`,
|
||||
// Add to geographics batch
|
||||
geographicsToCreate.push({
|
||||
district_id: districtCode,
|
||||
description: `Location for ${districtName} District in Jember (${year})`,
|
||||
address: address,
|
||||
type: 'district location',
|
||||
year: year,
|
||||
|
@ -181,26 +262,15 @@ export class GeoJSONSeeder {
|
|||
polygon: geometry,
|
||||
geometry: geometry,
|
||||
location: `POINT(${longitude} ${latitude})`,
|
||||
};
|
||||
|
||||
const { error } = await this.supabase
|
||||
.from('geographics')
|
||||
.insert([locationData])
|
||||
.select();
|
||||
|
||||
if (error) {
|
||||
console.error(
|
||||
`Error inserting into locations for district ${district.name} (${year}):`,
|
||||
error
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Inserted geographic data for: ${district.name} (${year}) with area: ${landArea} sq km`
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Insert all geographic data in smaller batches
|
||||
console.log(
|
||||
`Preparing to insert ${geographicsToCreate.length} geographic records in smaller batches`
|
||||
);
|
||||
await this.insertInBatches(geographicsToCreate);
|
||||
}
|
||||
|
||||
console.log(
|
||||
|
|
|
@ -1,90 +1,97 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
export class PermissionSeeder {
|
||||
constructor(private prisma: PrismaClient) { }
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
|
||||
async run(): Promise<void> {
|
||||
console.log('Seeding permissions...');
|
||||
async run(): Promise<void> {
|
||||
console.log('Seeding permissions...');
|
||||
|
||||
// Delete existing permissions to avoid duplicates
|
||||
await this.prisma.permissions.deleteMany({});
|
||||
// Delete existing permissions to avoid duplicates
|
||||
await this.prisma.permissions.deleteMany({});
|
||||
|
||||
try {
|
||||
// Fetch all resources and roles
|
||||
const allResources = await this.prisma.resources.findMany();
|
||||
const adminRole = await this.prisma.roles.findUnique({
|
||||
where: { name: 'admin' },
|
||||
});
|
||||
const viewerRole = await this.prisma.roles.findUnique({
|
||||
where: { name: 'viewer' },
|
||||
});
|
||||
const staffRole = await this.prisma.roles.findUnique({
|
||||
where: { name: 'staff' },
|
||||
});
|
||||
try {
|
||||
// Fetch all resources and roles
|
||||
const allResources = await this.prisma.resources.findMany();
|
||||
const adminRole = await this.prisma.roles.findUnique({
|
||||
where: { name: 'admin' },
|
||||
});
|
||||
const viewerRole = await this.prisma.roles.findUnique({
|
||||
where: { name: 'viewer' },
|
||||
});
|
||||
const staffRole = await this.prisma.roles.findUnique({
|
||||
where: { name: 'staff' },
|
||||
});
|
||||
|
||||
if (!adminRole || !viewerRole || !staffRole) {
|
||||
console.error('Roles not found. Please seed roles first.');
|
||||
return;
|
||||
}
|
||||
if (!adminRole || !viewerRole || !staffRole) {
|
||||
console.error('Roles not found. Please seed roles first.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Admin permissions - full access to all resources
|
||||
for (const resource of allResources) {
|
||||
await this.createPermissions(adminRole.id, resource.id, [
|
||||
// Admin permissions - full access to all resources
|
||||
for (const resource of allResources) {
|
||||
await this.createPermissions(adminRole.id, resource.id, [
|
||||
'create',
|
||||
'read',
|
||||
'update',
|
||||
'delete',
|
||||
]);
|
||||
}
|
||||
|
||||
// Viewer permissions - read-only access to all resources
|
||||
for (const resource of allResources) {
|
||||
await this.createPermissions(viewerRole.id, resource.id, ['read']);
|
||||
}
|
||||
|
||||
// Staff permissions - mixed permissions based on resource
|
||||
for (const resource of allResources) {
|
||||
if (
|
||||
['roles', 'permissions', 'resources', 'users'].includes(resource.name)
|
||||
) {
|
||||
// Staff can only read roles, permissions, resources, and users
|
||||
await this.createPermissions(staffRole.id, resource.id, ['read']);
|
||||
} else {
|
||||
// Staff can create, read, update but not delete other resources
|
||||
await this.createPermissions(staffRole.id, resource.id, [
|
||||
'create',
|
||||
'read',
|
||||
'update',
|
||||
'delete',
|
||||
]);
|
||||
}
|
||||
|
||||
// Viewer permissions - read-only access to all resources
|
||||
for (const resource of allResources) {
|
||||
await this.createPermissions(viewerRole.id, resource.id, ['read']);
|
||||
}
|
||||
|
||||
// Staff permissions - mixed permissions based on resource
|
||||
for (const resource of allResources) {
|
||||
if (
|
||||
['roles', 'permissions', 'resources', 'users'].includes(
|
||||
resource.name
|
||||
)
|
||||
) {
|
||||
// Staff can only read roles, permissions, resources, and users
|
||||
await this.createPermissions(staffRole.id, resource.id, ['read']);
|
||||
} else {
|
||||
// Staff can create, read, update but not delete other resources
|
||||
await this.createPermissions(staffRole.id, resource.id, [
|
||||
'create',
|
||||
'read',
|
||||
'update',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Permissions seeded successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error seeding permissions:', error);
|
||||
}
|
||||
|
||||
console.log('Permissions seeded successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error seeding permissions:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async createPermissions(roleId: string, resourceId: string, actions: string[]) {
|
||||
for (const action of actions) {
|
||||
try {
|
||||
const permission = await this.prisma.permissions.createMany({
|
||||
data: {
|
||||
action: action,
|
||||
resource_id: resourceId,
|
||||
role_id: roleId,
|
||||
},
|
||||
skipDuplicates: true, // Skip if the permission already exists
|
||||
});
|
||||
private async createPermissions(
|
||||
roleId: string,
|
||||
resourceId: string,
|
||||
actions: string[]
|
||||
) {
|
||||
try {
|
||||
// Prepare all permissions at once
|
||||
const permissionsData = actions.map((action) => ({
|
||||
action: action,
|
||||
resource_id: resourceId,
|
||||
role_id: roleId,
|
||||
}));
|
||||
|
||||
// Create all permissions in a single batch operation
|
||||
const result = await this.prisma.permissions.createMany({
|
||||
data: permissionsData,
|
||||
skipDuplicates: true, // Skip if the permission already exists
|
||||
});
|
||||
|
||||
console.log(`Created permission: ${action} for role ${roleId} on resource ${resourceId}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error creating permission for role ${roleId} on resource ${resourceId}:`, error);
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
`Created ${result.count} permissions for role ${roleId} on resource ${resourceId}: ${actions.join(', ')}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error creating permissions for role ${roleId} on resource ${resourceId}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -79,41 +79,6 @@ export class UnitSeeder {
|
|||
return;
|
||||
}
|
||||
|
||||
const location = await this.getUnitsLocation(city.name);
|
||||
|
||||
if (!location) {
|
||||
console.warn(`No location found for city: ${city.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const [lng, lat] = [location.lng, location.lat];
|
||||
const address = location.address;
|
||||
const phone = location.telepon?.replace(/-/g, '');
|
||||
|
||||
const newId = await generateIdWithDbCounter('units', {
|
||||
prefix: 'UT',
|
||||
segments: {
|
||||
sequentialDigits: 4,
|
||||
},
|
||||
format: '{prefix}-{sequence}',
|
||||
separator: '-',
|
||||
uniquenessStrategy: 'counter',
|
||||
});
|
||||
|
||||
let locationData: CreateLocationDto = {
|
||||
district_id: city.districts[0].id, // This will be replaced with Patrang's ID
|
||||
city_id: city.id,
|
||||
code_unit: newId,
|
||||
name: `Polres ${city.name}`,
|
||||
description: `Unit ${city.name} is categorized as POLRES and operates in the ${city.name} area.`,
|
||||
type: 'polres',
|
||||
address,
|
||||
phone,
|
||||
longitude: lng,
|
||||
latitude: lat,
|
||||
location: `POINT(${lng} ${lat})`,
|
||||
};
|
||||
|
||||
// Find the Patrang district
|
||||
const patrangDistrict = await this.prisma.districts.findFirst({
|
||||
where: {
|
||||
|
@ -127,33 +92,18 @@ export class UnitSeeder {
|
|||
return;
|
||||
}
|
||||
|
||||
locationData.district_id = patrangDistrict.id;
|
||||
// Prepare arrays for batch operations
|
||||
const unitsToInsert = [];
|
||||
|
||||
const { error } = await this.supabase
|
||||
.from('units')
|
||||
.insert([locationData])
|
||||
.select();
|
||||
// First, get the Polres unit data
|
||||
const polresLocation = await this.getUnitsLocation(city.name);
|
||||
|
||||
if (error) {
|
||||
console.error(`Error inserting into Supabase locations:`, error);
|
||||
return;
|
||||
}
|
||||
if (polresLocation) {
|
||||
const [lng, lat] = [polresLocation.lng, polresLocation.lat];
|
||||
const address = polresLocation.address;
|
||||
const phone = polresLocation.telepon?.replace(/-/g, '');
|
||||
|
||||
let district;
|
||||
|
||||
for (district of districts) {
|
||||
const location = await this.getUnitsLocation(district.name);
|
||||
|
||||
if (!location) {
|
||||
console.warn(`No location found for district: ${district.name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const [lng, lat] = [location.lng, location.lat];
|
||||
const address = location.address;
|
||||
const phone = location.telepon?.replace(/-/g, '');
|
||||
|
||||
const newId = await generateIdWithDbCounter('units', {
|
||||
const polresId = await generateIdWithDbCounter('units', {
|
||||
prefix: 'UT',
|
||||
segments: {
|
||||
sequentialDigits: 4,
|
||||
|
@ -163,7 +113,63 @@ export class UnitSeeder {
|
|||
uniquenessStrategy: 'counter',
|
||||
});
|
||||
|
||||
const locationData: CreateLocationDto = {
|
||||
unitsToInsert.push({
|
||||
district_id: patrangDistrict.id,
|
||||
city_id: city.id,
|
||||
code_unit: polresId,
|
||||
name: `Polres ${city.name}`,
|
||||
description: `Unit ${city.name} is categorized as POLRES and operates in the ${city.name} area.`,
|
||||
type: 'polres',
|
||||
address,
|
||||
phone,
|
||||
longitude: lng,
|
||||
latitude: lat,
|
||||
location: `POINT(${lng} ${lat})`,
|
||||
});
|
||||
} else {
|
||||
console.warn(`No location found for city: ${city.name}`);
|
||||
}
|
||||
|
||||
// Now prepare data for all Polseks
|
||||
const locationPromises = districts.map((district) =>
|
||||
this.getUnitsLocation(district.name)
|
||||
.then((location) => ({ district, location }))
|
||||
.catch(() => ({ district, location: null }))
|
||||
);
|
||||
|
||||
// Wait for all location lookups to complete
|
||||
const results = await Promise.all(locationPromises);
|
||||
|
||||
// Generate all IDs upfront
|
||||
const idPromises = Array(results.length)
|
||||
.fill(0)
|
||||
.map(() =>
|
||||
generateIdWithDbCounter('units', {
|
||||
prefix: 'UT',
|
||||
segments: {
|
||||
sequentialDigits: 4,
|
||||
},
|
||||
format: '{prefix}-{sequence}',
|
||||
separator: '-',
|
||||
uniquenessStrategy: 'counter',
|
||||
})
|
||||
);
|
||||
|
||||
const ids = await Promise.all(idPromises);
|
||||
|
||||
// Process results and add to unitsToInsert
|
||||
results.forEach(({ district, location }, index) => {
|
||||
if (!location) {
|
||||
console.warn(`No location found for district: ${district.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const [lng, lat] = [location.lng, location.lat];
|
||||
const address = location.address;
|
||||
const phone = location.telepon?.replace(/-/g, '');
|
||||
const newId = ids[index];
|
||||
|
||||
unitsToInsert.push({
|
||||
district_id: district.id,
|
||||
city_id: district.city_id,
|
||||
code_unit: newId,
|
||||
|
@ -175,21 +181,29 @@ export class UnitSeeder {
|
|||
longitude: lng,
|
||||
latitude: lat,
|
||||
location: `POINT(${lng} ${lat})`,
|
||||
};
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Prepared unit data for district: ${district.name}, ID: ${newId}`
|
||||
);
|
||||
});
|
||||
|
||||
// Insert all units in a single batch operation
|
||||
if (unitsToInsert.length > 0) {
|
||||
const { error } = await this.supabase
|
||||
.from('units')
|
||||
.insert([locationData])
|
||||
.insert(unitsToInsert)
|
||||
.select();
|
||||
|
||||
if (error) {
|
||||
console.error(`Error inserting into Supabase locations:`, error);
|
||||
continue;
|
||||
console.error(`Error batch inserting units into Supabase:`, error);
|
||||
} else {
|
||||
console.log(
|
||||
`Successfully inserted ${unitsToInsert.length} units in batch`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Inserted unit for district: ${district.name}, newId: ${newId} at ${lng}, ${lat}`
|
||||
);
|
||||
} else {
|
||||
console.warn('No unit data to insert');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -105,9 +105,9 @@ file_size_limit = "50MiB"
|
|||
enabled = true
|
||||
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
|
||||
# in emails.
|
||||
site_url = "https://127.0.0.1:3000"
|
||||
site_url = "http://127.0.0.1:3000"
|
||||
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
|
||||
additional_redirect_urls = []
|
||||
additional_redirect_urls = ["https://127.0.0.1:3000"]
|
||||
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
|
||||
jwt_expiry = 3600
|
||||
# If disabled, the refresh token will never expire.
|
||||
|
@ -122,7 +122,7 @@ enable_anonymous_sign_ins = false
|
|||
# Allow/disallow testing manual linking of accounts
|
||||
enable_manual_linking = false
|
||||
# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more.
|
||||
minimum_password_length = 8
|
||||
minimum_password_length = 6
|
||||
# Passwords that do not meet the following requirements will be rejected as weak. Supported values
|
||||
# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols`
|
||||
password_requirements = ""
|
||||
|
@ -154,7 +154,7 @@ enable_signup = true
|
|||
# addresses. If disabled, only the new email is required to confirm.
|
||||
double_confirm_changes = true
|
||||
# If enabled, users need to confirm their email address before signing in.
|
||||
enable_confirmations = true
|
||||
enable_confirmations = false
|
||||
# If enabled, users will need to reauthenticate or have logged in recently to change their password.
|
||||
secure_password_change = false
|
||||
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
|
||||
|
@ -162,7 +162,7 @@ max_frequency = "1s"
|
|||
# Number of characters used in the email OTP.
|
||||
otp_length = 6
|
||||
# Number of seconds before the email OTP expires (defaults to 1 hour).
|
||||
otp_expiry = 300
|
||||
otp_expiry = 3600
|
||||
|
||||
# Use a production-ready SMTP server
|
||||
# [auth.email.smtp]
|
||||
|
@ -220,8 +220,8 @@ max_enrolled_factors = 10
|
|||
|
||||
# Control MFA via App Authenticator (TOTP)
|
||||
[auth.mfa.totp]
|
||||
enroll_enabled = true
|
||||
verify_enabled = true
|
||||
enroll_enabled = false
|
||||
verify_enabled = false
|
||||
|
||||
# Configure MFA via Phone Messaging
|
||||
[auth.mfa.phone]
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
drop trigger if exists "on_auth_user_created" on "auth"."users";
|
||||
|
||||
drop trigger if exists "on_auth_user_deleted" on "auth"."users";
|
||||
|
||||
drop trigger if exists "on_auth_user_updated" on "auth"."users";
|
||||
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
-- Restore function: public.generate_username
|
||||
CREATE OR REPLACE FUNCTION public.generate_username(email text)
|
||||
RETURNS text
|
||||
LANGUAGE plpgsql SECURITY DEFINER
|
||||
AS $$
|
||||
DECLARE
|
||||
result_username TEXT;
|
||||
username_base TEXT;
|
||||
random_number INTEGER;
|
||||
username_exists BOOLEAN;
|
||||
BEGIN
|
||||
username_base := split_part(email, '@', 1);
|
||||
username_base := regexp_replace(username_base, '[^a-zA-Z0-9]', '_', 'g');
|
||||
random_number := floor(random() * 9900 + 100)::integer;
|
||||
result_username := username_base || random_number;
|
||||
LOOP
|
||||
SELECT EXISTS(SELECT 1 FROM public.profiles WHERE username = result_username) INTO username_exists;
|
||||
IF NOT username_exists THEN
|
||||
EXIT;
|
||||
END IF;
|
||||
random_number := floor(random() * 9900 + 100)::integer;
|
||||
result_username := username_base || random_number;
|
||||
END LOOP;
|
||||
RETURN result_username;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Restore function: gis.update_land_area
|
||||
CREATE OR REPLACE FUNCTION gis.update_land_area()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW.land_area := ROUND((ST_Area(NEW.geometry::geography) / 1000000.0)::numeric, 2);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Restore function: public.update_land_area
|
||||
CREATE OR REPLACE FUNCTION public.update_land_area()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW.land_area := ROUND(ST_Area(NEW.geometry::gis.geography) / 1000000.0);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Restore function: public.update_timestamp
|
||||
CREATE OR REPLACE FUNCTION public.update_timestamp()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Restore function: public.handle_new_user
|
||||
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql SECURITY DEFINER
|
||||
AS $$
|
||||
DECLARE
|
||||
role_id UUID;
|
||||
BEGIN
|
||||
SELECT id INTO role_id FROM public.roles WHERE name = 'viewer' LIMIT 1;
|
||||
IF role_id IS NULL THEN
|
||||
RAISE EXCEPTION 'Role not found';
|
||||
END IF;
|
||||
INSERT INTO public.users (
|
||||
id,
|
||||
roles_id,
|
||||
email,
|
||||
phone,
|
||||
encrypted_password,
|
||||
invited_at,
|
||||
confirmed_at,
|
||||
email_confirmed_at,
|
||||
recovery_sent_at,
|
||||
last_sign_in_at,
|
||||
app_metadata,
|
||||
user_metadata,
|
||||
created_at,
|
||||
updated_at,
|
||||
banned_until,
|
||||
is_anonymous
|
||||
) VALUES (
|
||||
NEW.id,
|
||||
role_id,
|
||||
NEW.email,
|
||||
NEW.phone,
|
||||
NEW.encrypted_password,
|
||||
NEW.invited_at,
|
||||
NEW.confirmed_at,
|
||||
NEW.email_confirmed_at,
|
||||
NEW.recovery_sent_at,
|
||||
NEW.last_sign_in_at,
|
||||
NEW.raw_app_meta_data,
|
||||
NEW.raw_user_meta_data,
|
||||
NEW.created_at,
|
||||
NEW.updated_at,
|
||||
NEW.banned_until,
|
||||
NEW.is_anonymous
|
||||
);
|
||||
INSERT INTO public.profiles (
|
||||
id,
|
||||
user_id,
|
||||
avatar,
|
||||
username,
|
||||
first_name,
|
||||
last_name,
|
||||
bio,
|
||||
address,
|
||||
birth_date
|
||||
) VALUES (
|
||||
gen_random_uuid(),
|
||||
NEW.id,
|
||||
NULL,
|
||||
public.generate_username(NEW.email),
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL
|
||||
);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Restore function: public.handle_user_delete
|
||||
CREATE OR REPLACE FUNCTION public.handle_user_delete()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql SECURITY DEFINER
|
||||
AS $$
|
||||
BEGIN
|
||||
DELETE FROM public.profiles WHERE user_id = OLD.id;
|
||||
DELETE FROM public.users WHERE id = OLD.id;
|
||||
RETURN OLD;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Restore function: public.handle_user_update
|
||||
CREATE OR REPLACE FUNCTION public.handle_user_update()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql SECURITY DEFINER
|
||||
AS $$
|
||||
BEGIN
|
||||
UPDATE public.users
|
||||
SET
|
||||
email = COALESCE(NEW.email, email),
|
||||
phone = COALESCE(NEW.phone, phone),
|
||||
encrypted_password = COALESCE(NEW.encrypted_password, encrypted_password),
|
||||
invited_at = COALESCE(NEW.invited_at, invited_at),
|
||||
confirmed_at = COALESCE(NEW.confirmed_at, confirmed_at),
|
||||
email_confirmed_at = COALESCE(NEW.email_confirmed_at, email_confirmed_at),
|
||||
recovery_sent_at = COALESCE(NEW.recovery_sent_at, recovery_sent_at),
|
||||
last_sign_in_at = COALESCE(NEW.last_sign_in_at, last_sign_in_at),
|
||||
app_metadata = COALESCE(NEW.raw_app_meta_data, app_metadata),
|
||||
user_metadata = COALESCE(NEW.raw_user_meta_data, user_metadata),
|
||||
created_at = COALESCE(NEW.created_at, created_at),
|
||||
updated_at = NOW(),
|
||||
banned_until = CASE
|
||||
WHEN NEW.banned_until IS NULL THEN NULL
|
||||
ELSE COALESCE(NEW.banned_until, banned_until)
|
||||
END,
|
||||
is_anonymous = COALESCE(NEW.is_anonymous, is_anonymous)
|
||||
WHERE id = NEW.id;
|
||||
INSERT INTO public.profiles (id, user_id)
|
||||
SELECT gen_random_uuid(), NEW.id
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM public.profiles WHERE user_id = NEW.id
|
||||
)
|
||||
ON CONFLICT (user_id) DO NOTHING;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Function: public.handle_new_user()
|
||||
-- Already exists in schema, so just create trigger
|
||||
|
||||
CREATE TRIGGER "on_auth_user_created"
|
||||
AFTER INSERT ON "auth"."users"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.handle_new_user();
|
||||
|
||||
-- Function: public.handle_user_delete()
|
||||
-- Already exists in schema, so just create trigger
|
||||
|
||||
CREATE TRIGGER "on_auth_user_deleted"
|
||||
AFTER DELETE ON "auth"."users"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.handle_user_delete();
|
||||
|
||||
-- Function: public.handle_user_update()
|
||||
-- Already exists in schema, so just create trigger
|
||||
|
||||
CREATE TRIGGER "on_auth_user_updated"
|
||||
AFTER UPDATE ON "auth"."users"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.handle_user_update();
|
|
@ -0,0 +1,163 @@
|
|||
-- drop type "gis"."geometry_dump";
|
||||
|
||||
-- drop type "gis"."valid_detail";
|
||||
|
||||
-- set check_function_bodies = off;
|
||||
|
||||
DROP FUNCTION IF EXISTS gis.calculate_unit_incident_distances(VARCHAR, VARCHAR);
|
||||
|
||||
DROP FUNCTION IF EXISTS gis.find_nearest_unit(character varying);
|
||||
|
||||
DROP FUNCTION IF EXISTS gis.find_units_within_distance(character varying, double precision);
|
||||
|
||||
-- DROP FUNCTION IF EXISTS gis.find_units_within_distance(character varying, double precision DEFAULT 5000);
|
||||
|
||||
|
||||
|
||||
CREATE OR REPLACE FUNCTION gis.calculate_unit_incident_distances(
|
||||
p_unit_id VARCHAR,
|
||||
p_district_id VARCHAR DEFAULT NULL
|
||||
)
|
||||
RETURNS TABLE (
|
||||
unit_code VARCHAR,
|
||||
incident_id VARCHAR,
|
||||
district_name VARCHAR,
|
||||
distance_meters FLOAT
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
WITH unit_locations AS (
|
||||
SELECT
|
||||
u.code_unit,
|
||||
u.district_id,
|
||||
ST_SetSRID(ST_MakePoint(u.longitude, u.latitude), 4326)::gis.geography AS location
|
||||
FROM
|
||||
units u
|
||||
WHERE
|
||||
(p_unit_id IS NULL OR u.code_unit = p_unit_id)
|
||||
AND (p_district_id IS NULL OR u.district_id = p_district_id)
|
||||
AND u.latitude IS NOT NULL
|
||||
AND u.longitude IS NOT NULL
|
||||
),
|
||||
incident_locations AS (
|
||||
SELECT
|
||||
ci.id,
|
||||
ci.crime_id,
|
||||
ci.crime_category_id,
|
||||
ST_SetSRID(ST_MakePoint(l.longitude, l.latitude), 4326)::gis.geography AS location
|
||||
FROM
|
||||
crime_incidents ci
|
||||
JOIN
|
||||
locations l ON ci.location_id = l.id
|
||||
WHERE
|
||||
l.latitude IS NOT NULL
|
||||
AND l.longitude IS NOT NULL
|
||||
)
|
||||
SELECT
|
||||
ul.code_unit as unit_code,
|
||||
il.id as incident_id,
|
||||
d.name as district_name,
|
||||
ST_Distance(ul.location, il.location) as distance_meters
|
||||
FROM
|
||||
unit_locations ul
|
||||
JOIN
|
||||
districts d ON ul.district_id = d.id
|
||||
JOIN
|
||||
crimes c ON c.district_id = d.id
|
||||
JOIN
|
||||
incident_locations il ON il.crime_id = c.id
|
||||
ORDER BY
|
||||
ul.code_unit,
|
||||
ul.location <-> il.location;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
|
||||
CREATE OR REPLACE FUNCTION gis.find_nearest_unit(p_incident_id character varying)
|
||||
RETURNS TABLE(unit_code character varying, unit_name character varying, distance_meters double precision)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $function$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
WITH incident_location AS (
|
||||
SELECT
|
||||
ci.id,
|
||||
l.location AS location
|
||||
FROM
|
||||
crime_incidents ci
|
||||
JOIN
|
||||
locations l ON ci.location_id = l.id
|
||||
WHERE
|
||||
ci.id = p_incident_id
|
||||
),
|
||||
unit_locations AS (
|
||||
SELECT
|
||||
u.code_unit,
|
||||
u.name,
|
||||
u.location
|
||||
FROM
|
||||
units u
|
||||
)
|
||||
SELECT
|
||||
ul.code_unit as unit_code,
|
||||
ul.name as unit_name,
|
||||
ST_Distance(ul.location, il.location) as distance_meters
|
||||
FROM
|
||||
unit_locations ul
|
||||
CROSS JOIN
|
||||
incident_location il
|
||||
ORDER BY
|
||||
ul.location <-> il.location
|
||||
LIMIT 1;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
CREATE OR REPLACE FUNCTION gis.find_units_within_distance(p_incident_id character varying, p_max_distance_meters double precision DEFAULT 5000)
|
||||
RETURNS TABLE(unit_code character varying, unit_name character varying, distance_meters double precision)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $function$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
WITH incident_location AS (
|
||||
SELECT
|
||||
ci.id,
|
||||
l.location AS location
|
||||
FROM
|
||||
crime_incidents ci
|
||||
JOIN
|
||||
locations l ON ci.location_id = l.id
|
||||
WHERE
|
||||
ci.id = p_incident_id
|
||||
),
|
||||
unit_locations AS (
|
||||
SELECT
|
||||
u.code_unit,
|
||||
u.name,
|
||||
u.location
|
||||
FROM
|
||||
units u
|
||||
)
|
||||
SELECT
|
||||
ul.code_unit as unit_code,
|
||||
ul.name as unit_name,
|
||||
ST_Distance(ul.location, il.location) as distance_meters
|
||||
FROM
|
||||
unit_locations ul
|
||||
CROSS JOIN
|
||||
incident_location il
|
||||
WHERE
|
||||
ST_DWithin(ul.location, il.location, p_max_distance_meters)
|
||||
ORDER BY
|
||||
ST_Distance(ul.location, il.location);
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
-- create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry);
|
||||
|
||||
-- create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry);
|
||||
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
-- drop type "gis"."geometry_dump";
|
||||
|
||||
-- drop type "gis"."valid_detail";
|
||||
|
||||
-- set check_function_bodies = off;
|
||||
|
||||
DROP FUNCTION IF EXISTS gis.calculate_unit_incident_distances(VARCHAR, VARCHAR);
|
||||
|
||||
DROP FUNCTION IF EXISTS gis.find_nearest_unit(character varying);
|
||||
|
||||
DROP FUNCTION IF EXISTS gis.find_units_within_distance(character varying, double precision);
|
||||
|
||||
-- DROP FUNCTION IF EXISTS gis.find_units_within_distance(character varying, double precision DEFAULT 5000);
|
||||
|
||||
|
||||
|
||||
CREATE OR REPLACE FUNCTION gis.calculate_unit_incident_distances(
|
||||
p_unit_id VARCHAR,
|
||||
p_district_id VARCHAR DEFAULT NULL
|
||||
)
|
||||
RETURNS TABLE (
|
||||
unit_code VARCHAR,
|
||||
incident_id VARCHAR,
|
||||
district_name VARCHAR,
|
||||
distance_meters FLOAT
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
WITH unit_locations AS (
|
||||
SELECT
|
||||
u.code_unit,
|
||||
u.district_id,
|
||||
ST_SetSRID(ST_MakePoint(u.longitude, u.latitude), 4326)::gis.geography AS location
|
||||
FROM
|
||||
units u
|
||||
WHERE
|
||||
(p_unit_id IS NULL OR u.code_unit = p_unit_id)
|
||||
AND (p_district_id IS NULL OR u.district_id = p_district_id)
|
||||
AND u.latitude IS NOT NULL
|
||||
AND u.longitude IS NOT NULL
|
||||
),
|
||||
incident_locations AS (
|
||||
SELECT
|
||||
ci.id,
|
||||
ci.crime_id,
|
||||
ci.crime_category_id,
|
||||
ST_SetSRID(ST_MakePoint(l.longitude, l.latitude), 4326)::gis.geography AS location
|
||||
FROM
|
||||
crime_incidents ci
|
||||
JOIN
|
||||
locations l ON ci.location_id = l.id
|
||||
WHERE
|
||||
l.latitude IS NOT NULL
|
||||
AND l.longitude IS NOT NULL
|
||||
)
|
||||
SELECT
|
||||
ul.code_unit as unit_code,
|
||||
il.id as incident_id,
|
||||
d.name as district_name,
|
||||
ST_Distance(ul.location, il.location) as distance_meters
|
||||
FROM
|
||||
unit_locations ul
|
||||
JOIN
|
||||
districts d ON ul.district_id = d.id
|
||||
JOIN
|
||||
crimes c ON c.district_id = d.id
|
||||
JOIN
|
||||
incident_locations il ON il.crime_id = c.id
|
||||
ORDER BY
|
||||
ul.code_unit,
|
||||
ul.location <-> il.location;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
|
||||
CREATE OR REPLACE FUNCTION gis.find_nearest_unit(p_incident_id character varying)
|
||||
RETURNS TABLE(unit_code character varying, unit_name character varying, distance_meters double precision)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $function$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
WITH incident_location AS (
|
||||
SELECT
|
||||
ci.id,
|
||||
l.location AS location
|
||||
FROM
|
||||
crime_incidents ci
|
||||
JOIN
|
||||
locations l ON ci.location_id = l.id
|
||||
WHERE
|
||||
ci.id = p_incident_id
|
||||
),
|
||||
unit_locations AS (
|
||||
SELECT
|
||||
u.code_unit,
|
||||
u.name,
|
||||
u.location
|
||||
FROM
|
||||
units u
|
||||
)
|
||||
SELECT
|
||||
ul.code_unit as unit_code,
|
||||
ul.name as unit_name,
|
||||
ST_Distance(ul.location, il.location) as distance_meters
|
||||
FROM
|
||||
unit_locations ul
|
||||
CROSS JOIN
|
||||
incident_location il
|
||||
ORDER BY
|
||||
ul.location <-> il.location
|
||||
LIMIT 1;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
CREATE OR REPLACE FUNCTION gis.find_units_within_distance(p_incident_id character varying, p_max_distance_meters double precision DEFAULT 5000)
|
||||
RETURNS TABLE(unit_code character varying, unit_name character varying, distance_meters double precision)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $function$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
WITH incident_location AS (
|
||||
SELECT
|
||||
ci.id,
|
||||
l.location AS location
|
||||
FROM
|
||||
crime_incidents ci
|
||||
JOIN
|
||||
locations l ON ci.location_id = l.id
|
||||
WHERE
|
||||
ci.id = p_incident_id
|
||||
),
|
||||
unit_locations AS (
|
||||
SELECT
|
||||
u.code_unit,
|
||||
u.name,
|
||||
u.location
|
||||
FROM
|
||||
units u
|
||||
)
|
||||
SELECT
|
||||
ul.code_unit as unit_code,
|
||||
ul.name as unit_name,
|
||||
ST_Distance(ul.location, il.location) as distance_meters
|
||||
FROM
|
||||
unit_locations ul
|
||||
CROSS JOIN
|
||||
incident_location il
|
||||
WHERE
|
||||
ST_DWithin(ul.location, il.location, p_max_distance_meters)
|
||||
ORDER BY
|
||||
ST_Distance(ul.location, il.location);
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
-- create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry);
|
||||
|
||||
-- create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry);
|
||||
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
-- drop type "gis"."geometry_dump";
|
||||
|
||||
-- drop type "gis"."valid_detail";
|
||||
|
||||
-- create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry);
|
||||
|
||||
-- create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry);
|
||||
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
-- drop type "gis"."geometry_dump";
|
||||
|
||||
-- drop type "gis"."valid_detail";
|
||||
|
||||
-- set check_function_bodies = off;
|
||||
|
||||
DROP FUNCTION IF EXISTS gis.calculate_unit_incident_distances(VARCHAR, VARCHAR);
|
||||
|
||||
DROP FUNCTION IF EXISTS gis.find_nearest_unit(character varying);
|
||||
|
||||
DROP FUNCTION IF EXISTS gis.find_units_within_distance(character varying, double precision);
|
||||
|
||||
-- DROP FUNCTION IF EXISTS gis.find_units_within_distance(character varying, double precision DEFAULT 5000);
|
||||
|
||||
|
||||
|
||||
CREATE OR REPLACE FUNCTION gis.calculate_unit_incident_distances(
|
||||
p_unit_id VARCHAR,
|
||||
p_district_id VARCHAR DEFAULT NULL
|
||||
)
|
||||
RETURNS TABLE (
|
||||
unit_code VARCHAR,
|
||||
incident_id VARCHAR,
|
||||
district_name VARCHAR,
|
||||
distance_meters FLOAT
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
WITH unit_locations AS (
|
||||
SELECT
|
||||
u.code_unit,
|
||||
u.district_id,
|
||||
ST_SetSRID(ST_MakePoint(u.longitude, u.latitude), 4326)::gis.geography AS location
|
||||
FROM
|
||||
units u
|
||||
WHERE
|
||||
(p_unit_id IS NULL OR u.code_unit = p_unit_id)
|
||||
AND (p_district_id IS NULL OR u.district_id = p_district_id)
|
||||
AND u.latitude IS NOT NULL
|
||||
AND u.longitude IS NOT NULL
|
||||
),
|
||||
incident_locations AS (
|
||||
SELECT
|
||||
ci.id,
|
||||
ci.crime_id,
|
||||
ci.crime_category_id,
|
||||
ST_SetSRID(ST_MakePoint(l.longitude, l.latitude), 4326)::gis.geography AS location
|
||||
FROM
|
||||
crime_incidents ci
|
||||
JOIN
|
||||
locations l ON ci.location_id = l.id
|
||||
WHERE
|
||||
l.latitude IS NOT NULL
|
||||
AND l.longitude IS NOT NULL
|
||||
)
|
||||
SELECT
|
||||
ul.code_unit as unit_code,
|
||||
il.id as incident_id,
|
||||
d.name as district_name,
|
||||
ST_Distance(ul.location, il.location) as distance_meters
|
||||
FROM
|
||||
unit_locations ul
|
||||
JOIN
|
||||
districts d ON ul.district_id = d.id
|
||||
JOIN
|
||||
crimes c ON c.district_id = d.id
|
||||
JOIN
|
||||
incident_locations il ON il.crime_id = c.id
|
||||
ORDER BY
|
||||
ul.code_unit,
|
||||
ul.location <-> il.location;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
|
||||
CREATE OR REPLACE FUNCTION gis.find_nearest_unit(p_incident_id character varying)
|
||||
RETURNS TABLE(unit_code character varying, unit_name character varying, distance_meters double precision)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $function$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
WITH incident_location AS (
|
||||
SELECT
|
||||
ci.id,
|
||||
l.location AS location
|
||||
FROM
|
||||
crime_incidents ci
|
||||
JOIN
|
||||
locations l ON ci.location_id = l.id
|
||||
WHERE
|
||||
ci.id = p_incident_id
|
||||
),
|
||||
unit_locations AS (
|
||||
SELECT
|
||||
u.code_unit,
|
||||
u.name,
|
||||
u.location
|
||||
FROM
|
||||
units u
|
||||
)
|
||||
SELECT
|
||||
ul.code_unit as unit_code,
|
||||
ul.name as unit_name,
|
||||
ST_Distance(ul.location, il.location) as distance_meters
|
||||
FROM
|
||||
unit_locations ul
|
||||
CROSS JOIN
|
||||
incident_location il
|
||||
ORDER BY
|
||||
ul.location <-> il.location
|
||||
LIMIT 1;
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
CREATE OR REPLACE FUNCTION gis.find_units_within_distance(p_incident_id character varying, p_max_distance_meters double precision DEFAULT 5000)
|
||||
RETURNS TABLE(unit_code character varying, unit_name character varying, distance_meters double precision)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $function$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
WITH incident_location AS (
|
||||
SELECT
|
||||
ci.id,
|
||||
l.location AS location
|
||||
FROM
|
||||
crime_incidents ci
|
||||
JOIN
|
||||
locations l ON ci.location_id = l.id
|
||||
WHERE
|
||||
ci.id = p_incident_id
|
||||
),
|
||||
unit_locations AS (
|
||||
SELECT
|
||||
u.code_unit,
|
||||
u.name,
|
||||
u.location
|
||||
FROM
|
||||
units u
|
||||
)
|
||||
SELECT
|
||||
ul.code_unit as unit_code,
|
||||
ul.name as unit_name,
|
||||
ST_Distance(ul.location, il.location) as distance_meters
|
||||
FROM
|
||||
unit_locations ul
|
||||
CROSS JOIN
|
||||
incident_location il
|
||||
WHERE
|
||||
ST_DWithin(ul.location, il.location, p_max_distance_meters)
|
||||
ORDER BY
|
||||
ST_Distance(ul.location, il.location);
|
||||
END;
|
||||
$function$
|
||||
;
|
||||
|
||||
-- create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry);
|
||||
|
||||
-- create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry);
|
||||
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
drop type "gis"."geometry_dump";
|
||||
|
||||
drop type "gis"."valid_detail";
|
||||
|
||||
create type "gis"."geometry_dump" as ("path" integer[], "geom" geometry);
|
||||
|
||||
create type "gis"."valid_detail" as ("valid" boolean, "reason" character varying, "location" geometry);
|
||||
|
||||
|
Loading…
Reference in New Issue