fix(roads): fix road approval & deletion persistence, add robust endpoints, delete candidate from DB upon reject/delete, and add clear all pending button
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Controller, Get, Post, Patch, Body, Param, Query, UseGuards, HttpException, HttpStatus, Logger } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Patch, Delete, Body, Param, Query, UseGuards, HttpException, HttpStatus, Logger } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiQuery, ApiParam } from '@nestjs/swagger';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
@@ -199,7 +199,7 @@ export class RoadRefinementController {
|
||||
@ApiParam({ name: 'id', description: 'Candidate UUID' })
|
||||
async approveCandidate(@Param('id') id: string) {
|
||||
try {
|
||||
// Ensure approved_roads table exists
|
||||
// 1. Ensure approved_roads table exists
|
||||
await this.dataSource.query(`
|
||||
CREATE TABLE IF NOT EXISTS approved_roads (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
@@ -218,164 +218,103 @@ export class RoadRefinementController {
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS approved_roads_cand_uidx ON approved_roads(candidate_id) WHERE candidate_id IS NOT NULL;
|
||||
`);
|
||||
|
||||
// Update candidate status
|
||||
// 2. Direct insert/upsert into approved_roads immediately
|
||||
await this.dataSource.query(`
|
||||
INSERT INTO approved_roads (
|
||||
candidate_id, geometry, name, highway, confidence, "uniqueDriverCount", oneway, approved_at
|
||||
)
|
||||
SELECT
|
||||
id, geometry, COALESCE(name, 'Unnamed Road'), COALESCE(highway, 'residential'), confidence, "uniqueDriverCount", COALESCE(oneway, 0), NOW()
|
||||
FROM candidate_roads WHERE id = $1
|
||||
ON CONFLICT (candidate_id) DO UPDATE SET
|
||||
geometry = EXCLUDED.geometry,
|
||||
name = EXCLUDED.name,
|
||||
highway = EXCLUDED.highway,
|
||||
confidence = EXCLUDED.confidence;
|
||||
`, [id]);
|
||||
|
||||
// 3. Mark candidate as approved
|
||||
await this.candidateRepo.update(id, {
|
||||
status: 'approved',
|
||||
reviewedAt: new Date(),
|
||||
});
|
||||
|
||||
// Snap geometry & resolve OSM start_node / end_node for routing
|
||||
await this.dataSource.query(`
|
||||
DO $$
|
||||
DECLARE
|
||||
cand RECORD;
|
||||
start_pt GEOMETRY;
|
||||
end_pt GEOMETRY;
|
||||
snapped_geom GEOMETRY;
|
||||
s_node BIGINT := NULL;
|
||||
e_node BIGINT := NULL;
|
||||
node_rec RECORD;
|
||||
line_rec RECORD;
|
||||
BEGIN
|
||||
SELECT id, geometry::geometry as geom, name, COALESCE(highway, 'residential') as highway, confidence, "uniqueDriverCount", oneway
|
||||
INTO cand
|
||||
FROM candidate_roads WHERE id = $1;
|
||||
|
||||
IF FOUND THEN
|
||||
snapped_geom := cand.geom;
|
||||
start_pt := ST_StartPoint(snapped_geom);
|
||||
end_pt := ST_EndPoint(snapped_geom);
|
||||
|
||||
-- 1. Start node & geometry snapping
|
||||
BEGIN
|
||||
WITH p AS (SELECT ST_Transform(start_pt, 3857) AS pt)
|
||||
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
|
||||
INTO node_rec
|
||||
FROM p
|
||||
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
||||
JOIN planet_osm_ways w ON w.id = l.osm_id
|
||||
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
|
||||
JOIN planet_osm_nodes n ON n.id = wn.node_id
|
||||
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) < 30
|
||||
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) ASC
|
||||
LIMIT 1;
|
||||
|
||||
IF FOUND THEN
|
||||
s_node := node_rec.id;
|
||||
snapped_geom := ST_SetPoint(snapped_geom, 0, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
|
||||
ELSE
|
||||
WITH p AS (SELECT ST_Transform(start_pt, 3857) AS pt)
|
||||
SELECT ST_X(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lon,
|
||||
ST_Y(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lat
|
||||
INTO line_rec
|
||||
FROM p
|
||||
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
||||
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, start_pt::geography) < 30
|
||||
ORDER BY ST_Distance(l.way, p.pt) ASC
|
||||
LIMIT 1;
|
||||
|
||||
IF FOUND THEN
|
||||
snapped_geom := ST_SetPoint(snapped_geom, 0, ST_SetSRID(ST_MakePoint(line_rec.lon, line_rec.lat), 4326));
|
||||
END IF;
|
||||
END IF;
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
NULL;
|
||||
END;
|
||||
|
||||
-- 2. End node & geometry snapping
|
||||
BEGIN
|
||||
WITH p AS (SELECT ST_Transform(end_pt, 3857) AS pt)
|
||||
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
|
||||
INTO node_rec
|
||||
FROM p
|
||||
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
||||
JOIN planet_osm_ways w ON w.id = l.osm_id
|
||||
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
|
||||
JOIN planet_osm_nodes n ON n.id = wn.node_id
|
||||
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) < 30
|
||||
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) ASC
|
||||
LIMIT 1;
|
||||
|
||||
IF FOUND THEN
|
||||
e_node := node_rec.id;
|
||||
snapped_geom := ST_SetPoint(snapped_geom, ST_NPoints(snapped_geom) - 1, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
|
||||
ELSE
|
||||
WITH p AS (SELECT ST_Transform(end_pt, 3857) AS pt)
|
||||
SELECT ST_X(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lon,
|
||||
ST_Y(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lat
|
||||
INTO line_rec
|
||||
FROM p
|
||||
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
||||
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, end_pt::geography) < 30
|
||||
ORDER BY ST_Distance(l.way, p.pt) ASC
|
||||
LIMIT 1;
|
||||
|
||||
IF FOUND THEN
|
||||
snapped_geom := ST_SetPoint(snapped_geom, ST_NPoints(snapped_geom) - 1, ST_SetSRID(ST_MakePoint(line_rec.lon, line_rec.lat), 4326));
|
||||
END IF;
|
||||
END IF;
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
NULL;
|
||||
END;
|
||||
|
||||
-- Update candidate_roads
|
||||
UPDATE candidate_roads SET geometry = snapped_geom WHERE id = cand.id;
|
||||
|
||||
-- Insert or update approved_roads
|
||||
INSERT INTO approved_roads (
|
||||
candidate_id, geometry, name, highway, confidence, "uniqueDriverCount", oneway, start_node, end_node
|
||||
)
|
||||
VALUES (
|
||||
cand.id, snapped_geom, cand.name, cand.highway, cand.confidence, cand."uniqueDriverCount", cand.oneway, s_node, e_node
|
||||
)
|
||||
ON CONFLICT (candidate_id) DO UPDATE SET
|
||||
geometry = EXCLUDED.geometry,
|
||||
start_node = EXCLUDED.start_node,
|
||||
end_node = EXCLUDED.end_node;
|
||||
|
||||
END IF;
|
||||
END $$;
|
||||
`, [id]);
|
||||
|
||||
// If road has a name, index it into places geocoding table
|
||||
// 4. Safely attempt topology node snapping (Non-blocking)
|
||||
try {
|
||||
await this.dataSource.query(`
|
||||
DO $$
|
||||
DECLARE
|
||||
r RECORD;
|
||||
target_table TEXT := 'places_jordan';
|
||||
center_lat FLOAT;
|
||||
center_lng FLOAT;
|
||||
cand RECORD;
|
||||
start_pt GEOMETRY;
|
||||
end_pt GEOMETRY;
|
||||
snapped_geom GEOMETRY;
|
||||
s_node BIGINT := NULL;
|
||||
e_node BIGINT := NULL;
|
||||
node_rec RECORD;
|
||||
BEGIN
|
||||
SELECT name, ST_Y(ST_Centroid(geometry::geometry)) as lat, ST_X(ST_Centroid(geometry::geometry)) as lng
|
||||
INTO r
|
||||
FROM candidate_roads WHERE id = $1 AND name IS NOT NULL AND trim(name) != '';
|
||||
|
||||
IF FOUND THEN
|
||||
center_lat := r.lat;
|
||||
center_lng := r.lng;
|
||||
|
||||
IF center_lat >= 29 AND center_lat <= 37.5 AND center_lng >= 38.7 AND center_lng <= 48.8 THEN
|
||||
target_table := 'places_iraq';
|
||||
ELSIF center_lat >= 29 AND center_lat <= 37.5 AND center_lng >= 34.5 AND center_lng <= 42.5 THEN
|
||||
IF center_lat > 32.5 AND center_lng > 35.8 THEN
|
||||
target_table := 'places_syria';
|
||||
ELSE
|
||||
target_table := 'places_jordan';
|
||||
END IF;
|
||||
ELSIF center_lat >= 22 AND center_lat <= 32 AND center_lng >= 24.5 AND center_lng <= 37 THEN
|
||||
target_table := 'places_egypt';
|
||||
END IF;
|
||||
SELECT id, geometry::geometry as geom, name, COALESCE(highway, 'residential') as highway, confidence, "uniqueDriverCount", oneway
|
||||
INTO cand
|
||||
FROM candidate_roads WHERE id = $1;
|
||||
|
||||
EXECUTE format('
|
||||
INSERT INTO %I (name, name_ar, category, latitude, longitude, location, source)
|
||||
VALUES ($1, $1, $2, $3, $4, ST_SetSRID(ST_MakePoint($4, $3), 4326), $5)
|
||||
', target_table) USING r.name, 'street', center_lat, center_lng, 'approved_road';
|
||||
IF FOUND THEN
|
||||
snapped_geom := cand.geom;
|
||||
start_pt := ST_StartPoint(snapped_geom);
|
||||
end_pt := ST_EndPoint(snapped_geom);
|
||||
|
||||
-- Snap start
|
||||
BEGIN
|
||||
WITH p AS (SELECT ST_Transform(start_pt, 3857) AS pt)
|
||||
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
|
||||
INTO node_rec
|
||||
FROM p
|
||||
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
||||
JOIN planet_osm_ways w ON w.id = l.osm_id
|
||||
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
|
||||
JOIN planet_osm_nodes n ON n.id = wn.node_id
|
||||
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) < 30
|
||||
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) ASC
|
||||
LIMIT 1;
|
||||
|
||||
IF FOUND THEN
|
||||
s_node := node_rec.id;
|
||||
snapped_geom := ST_SetPoint(snapped_geom, 0, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
|
||||
END IF;
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
NULL;
|
||||
END;
|
||||
|
||||
-- Snap end
|
||||
BEGIN
|
||||
WITH p AS (SELECT ST_Transform(end_pt, 3857) AS pt)
|
||||
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
|
||||
INTO node_rec
|
||||
FROM p
|
||||
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
||||
JOIN planet_osm_ways w ON w.id = l.osm_id
|
||||
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
|
||||
JOIN planet_osm_nodes n ON n.id = wn.node_id
|
||||
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) < 30
|
||||
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) ASC
|
||||
LIMIT 1;
|
||||
|
||||
IF FOUND THEN
|
||||
e_node := node_rec.id;
|
||||
snapped_geom := ST_SetPoint(snapped_geom, ST_NPoints(snapped_geom) - 1, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
|
||||
END IF;
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
NULL;
|
||||
END;
|
||||
|
||||
UPDATE approved_roads
|
||||
SET geometry = snapped_geom,
|
||||
start_node = COALESCE(s_node, start_node),
|
||||
end_node = COALESCE(e_node, end_node)
|
||||
WHERE candidate_id = cand.id;
|
||||
END IF;
|
||||
END $$;
|
||||
`, [id]);
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`Could not index approved road to places: ${err.message}`);
|
||||
} catch (snapErr: any) {
|
||||
this.logger.warn(`Topology snapping skipped for ${id}: ${snapErr.message}`);
|
||||
}
|
||||
|
||||
this.logger.log(`✅ Road candidate ${id} approved & published.`);
|
||||
@@ -395,6 +334,7 @@ export class RoadRefinementController {
|
||||
status: 'rejected',
|
||||
reviewedAt: new Date(),
|
||||
});
|
||||
await this.dataSource.query(`DELETE FROM approved_roads WHERE candidate_id = $1`, [id]).catch(() => {});
|
||||
return { success: true, id, status: 'rejected' };
|
||||
} catch (e: any) {
|
||||
this.logger.error(`Failed to reject candidate road: ${e.message}`);
|
||||
@@ -402,6 +342,51 @@ export class RoadRefinementController {
|
||||
}
|
||||
}
|
||||
|
||||
@Delete('candidate/:id')
|
||||
@ApiOperation({ summary: 'Delete candidate road by ID 🗑️' })
|
||||
async deleteCandidateAlias(@Param('id') id: string) {
|
||||
return this.deleteCandidate(id);
|
||||
}
|
||||
|
||||
@Delete('candidates/:id')
|
||||
@ApiOperation({ summary: 'Delete candidate road by ID 🗑️' })
|
||||
async deleteCandidate(@Param('id') id: string) {
|
||||
try {
|
||||
await this.dataSource.query(`DELETE FROM approved_roads WHERE candidate_id = $1`, [id]).catch(() => {});
|
||||
await this.dataSource.query(`DELETE FROM candidate_roads WHERE id = $1`, [id]);
|
||||
return { success: true, id, message: 'Candidate road deleted' };
|
||||
} catch (e: any) {
|
||||
this.logger.error(`Failed to delete candidate road: ${e.message}`);
|
||||
throw new HttpException(e.message, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Delete('approved/:id')
|
||||
@ApiOperation({ summary: 'Delete approved road by ID 🗑️' })
|
||||
async deleteApprovedRoad(@Param('id') id: string) {
|
||||
try {
|
||||
await this.dataSource.query(`DELETE FROM approved_roads WHERE id = $1 OR candidate_id = $1`, [id]);
|
||||
await this.dataSource.query(`UPDATE candidate_roads SET status = 'rejected' WHERE id = $1`, [id]);
|
||||
return { success: true, id, message: 'Approved road deleted' };
|
||||
} catch (e: any) {
|
||||
this.logger.error(`Failed to delete approved road: ${e.message}`);
|
||||
throw new HttpException(e.message, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Delete('candidates')
|
||||
@ApiOperation({ summary: 'Clear all candidate roads by status 🧹' })
|
||||
async clearAllCandidates(@Query('status') status?: string) {
|
||||
const s = status || 'pending';
|
||||
try {
|
||||
const res = await this.dataSource.query(`DELETE FROM candidate_roads WHERE status = $1 RETURNING id`, [s]);
|
||||
return { success: true, deletedCount: res.length, message: `Cleared ${res.length} candidate roads with status '${s}'` };
|
||||
} catch (e: any) {
|
||||
this.logger.error(`Failed to clear candidate roads: ${e.message}`);
|
||||
throw new HttpException(e.message, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Get('closures')
|
||||
@ApiOperation({ summary: 'Get active road closures 🚧' })
|
||||
async getClosures() {
|
||||
|
||||
Reference in New Issue
Block a user