update editor and add tests
This commit is contained in:
@@ -24,10 +24,8 @@ const UPDATE_METRICS_SQL = `UPDATE routes SET
|
||||
function trimEdgeHyphens(value) {
|
||||
let start = 0;
|
||||
let end = value.length;
|
||||
|
||||
while (start < end && value[start] === '-') start += 1;
|
||||
while (end > start && value[end - 1] === '-') end -= 1;
|
||||
|
||||
return value.slice(start, end);
|
||||
}
|
||||
|
||||
@@ -45,7 +43,6 @@ const audioUrl = (routeId, poiId) => `/api/routes/${routeId}/pois/${poiId}/audio
|
||||
|
||||
function rowToRoute(row, includeDeleted = false) {
|
||||
if (!row || (!includeDeleted && row.status !== 'active')) return null;
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
slug: row.slug,
|
||||
@@ -53,6 +50,7 @@ function rowToRoute(row, includeDeleted = false) {
|
||||
description: row.description,
|
||||
schoolName: row.school_name,
|
||||
status: row.status,
|
||||
activatedAt: row.activated_at,
|
||||
start: row.start_lat == null ? null : { lat: row.start_lat, lon: row.start_lon },
|
||||
center: row.center_lat == null ? null : { lat: row.center_lat, lon: row.center_lon },
|
||||
bounds: row.min_lat == null
|
||||
@@ -101,19 +99,16 @@ function uniqueSlug(db, source) {
|
||||
const base = slugify(source);
|
||||
let candidate = base;
|
||||
let suffix = 2;
|
||||
|
||||
while (db.prepare('SELECT 1 FROM routes WHERE slug = ?').get(candidate)) {
|
||||
candidate = `${base}-${suffix}`;
|
||||
suffix += 1;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function cleanupFailedRoute(db, routeId, gpxFile) {
|
||||
removeUpload(gpxFile);
|
||||
if (!routeId) return;
|
||||
|
||||
db.prepare('DELETE FROM route_points WHERE route_id = ?').run(routeId);
|
||||
db.prepare('DELETE FROM routes WHERE id = ?').run(routeId);
|
||||
fs.rmSync(routeDirectory(routeId), { recursive: true, force: true });
|
||||
@@ -131,13 +126,18 @@ export function listRoutes(db, {
|
||||
lat,
|
||||
lon,
|
||||
radiusKm = config.defaultRouteRadiusKm,
|
||||
includeDeleted = false
|
||||
includeDeleted = false,
|
||||
publishedOnly = false
|
||||
}) {
|
||||
const where = includeDeleted ? '' : "WHERE status = 'active'";
|
||||
let where = '';
|
||||
if (!includeDeleted) {
|
||||
where = publishedOnly
|
||||
? "WHERE status = 'active' AND activated_at IS NOT NULL"
|
||||
: "WHERE status = 'active'";
|
||||
}
|
||||
const rows = db.prepare(`SELECT * FROM routes ${where} ORDER BY name COLLATE NOCASE`).all();
|
||||
const origin = Number.isFinite(lat) && Number.isFinite(lon) ? { lat, lon } : null;
|
||||
const maximumDistance = radiusKm * 1000;
|
||||
|
||||
return rows
|
||||
.map(row => {
|
||||
const route = rowToRoute(row, includeDeleted);
|
||||
@@ -161,7 +161,6 @@ export function getRoute(db, id, includeDeleted = false) {
|
||||
export function getRouteGpxFile(db, id) {
|
||||
const route = db.prepare("SELECT id, slug, gpx_path FROM routes WHERE id = ? AND status = 'active'").get(id);
|
||||
if (!route) throw new HttpError(404, 'Strecke nicht gefunden.');
|
||||
|
||||
return {
|
||||
absolutePath: resolveStoredFile(route.gpx_path),
|
||||
filename: `${route.slug}.gpx`
|
||||
@@ -170,13 +169,11 @@ export function getRouteGpxFile(db, id) {
|
||||
|
||||
export function createRoute(db, fields, gpxFile) {
|
||||
if (!gpxFile) throw new HttpError(400, 'Eine GPX-Datei ist erforderlich.');
|
||||
|
||||
let routeId;
|
||||
try {
|
||||
const parsed = parseGpx(fs.readFileSync(gpxFile.path, 'utf8'));
|
||||
const name = String(fields.name || parsed.name || '').trim();
|
||||
if (!name) throw new HttpError(400, 'Ein Streckenname ist erforderlich.');
|
||||
|
||||
routeId = transaction(db, () => {
|
||||
const slug = uniqueSlug(db, fields.slug || name);
|
||||
const metrics = parsed.metrics;
|
||||
@@ -205,7 +202,6 @@ export function createRoute(db, fields, gpxFile) {
|
||||
insertRoutePoints(db, id, parsed.points);
|
||||
return id;
|
||||
});
|
||||
|
||||
ensureRouteDirectories(routeId);
|
||||
const gpxPath = moveUploadedFile(gpxFile, path.join(routeDirectory(routeId), 'route.gpx'));
|
||||
db.prepare('UPDATE routes SET gpx_path = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
|
||||
@@ -220,7 +216,6 @@ export function createRoute(db, fields, gpxFile) {
|
||||
export function updateRoute(db, id, fields, gpxFile) {
|
||||
const existing = db.prepare("SELECT * FROM routes WHERE id = ? AND status = 'active'").get(id);
|
||||
if (!existing) throw new HttpError(404, 'Strecke nicht gefunden.');
|
||||
|
||||
const parsed = gpxFile ? parseGpx(fs.readFileSync(gpxFile.path, 'utf8')) : null;
|
||||
transaction(db, () => {
|
||||
db.prepare(`UPDATE routes SET name = ?, description = ?, school_name = ?, updated_at = CURRENT_TIMESTAMP
|
||||
@@ -230,7 +225,6 @@ export function updateRoute(db, id, fields, gpxFile) {
|
||||
fields.schoolName ?? existing.school_name,
|
||||
id
|
||||
);
|
||||
|
||||
if (!parsed) return;
|
||||
db.prepare('DELETE FROM route_points WHERE route_id = ?').run(id);
|
||||
insertRoutePoints(db, id, parsed.points);
|
||||
@@ -243,7 +237,6 @@ export function updateRoute(db, id, fields, gpxFile) {
|
||||
|
||||
export function appendRoute(db, id, gpxFile) {
|
||||
if (!gpxFile) throw new HttpError(400, 'Eine GPX-Datei ist erforderlich.');
|
||||
|
||||
const existing = db.prepare("SELECT id FROM routes WHERE id = ? AND status = 'active'").get(id);
|
||||
if (!existing) throw new HttpError(404, 'Strecke nicht gefunden.');
|
||||
|
||||
@@ -251,7 +244,6 @@ export function appendRoute(db, id, gpxFile) {
|
||||
const current = db.prepare(`SELECT lat, lon, elevation, recorded_at AS recordedAt
|
||||
FROM route_points WHERE route_id = ? ORDER BY sequence`).all(id);
|
||||
const allPoints = current.concat(parsed.points).map((point, sequence) => ({ ...point, sequence }));
|
||||
|
||||
transaction(db, () => {
|
||||
insertRoutePoints(db, id, parsed.points, current.length);
|
||||
updateMetrics(db, id, routeMetrics(allPoints));
|
||||
@@ -265,7 +257,6 @@ export function listPois(db, routeId) {
|
||||
const pois = db.prepare('SELECT * FROM pois WHERE route_id = ? ORDER BY sequence, id').all(routeId);
|
||||
const images = db.prepare(`SELECT id, path, caption, sequence FROM poi_images
|
||||
WHERE poi_id = ? ORDER BY sequence, id`);
|
||||
|
||||
return pois.map(poi => ({
|
||||
id: poi.id,
|
||||
routeId: poi.route_id,
|
||||
@@ -301,7 +292,6 @@ export function createPoi(db, routeId, fields) {
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
|
||||
throw new HttpError(400, 'Gültige POI-Koordinaten sind erforderlich.');
|
||||
}
|
||||
|
||||
const result = db.prepare(`INSERT INTO pois
|
||||
(route_id, title, description, lat, lon, trigger_radius_m, sequence) VALUES (?, ?, ?, ?, ?, ?, ?)`).run(
|
||||
routeId,
|
||||
@@ -319,7 +309,6 @@ export function updatePoi(db, routeId, poiId, fields) {
|
||||
const existing = db.prepare(`SELECT p.*, r.status FROM pois p JOIN routes r ON r.id = p.route_id
|
||||
WHERE p.id = ? AND p.route_id = ?`).get(poiId, routeId);
|
||||
if (existing?.status !== 'active') throw new HttpError(404, 'POI nicht gefunden.');
|
||||
|
||||
const lat = fields.lat == null ? existing.lat : Number.parseFloat(fields.lat);
|
||||
const lon = fields.lon == null ? existing.lon : Number.parseFloat(fields.lon);
|
||||
const radius = fields.triggerRadiusM == null
|
||||
@@ -329,7 +318,6 @@ export function updatePoi(db, routeId, poiId, fields) {
|
||||
if (![lat, lon, radius].every(Number.isFinite) || !Number.isInteger(sequence) || sequence < 0) {
|
||||
throw new HttpError(400, 'POI-Koordinaten, Auslöseradius oder Reihenfolge sind ungültig.');
|
||||
}
|
||||
|
||||
db.prepare(`UPDATE pois SET title=?, description=?, lat=?, lon=?, trigger_radius_m=?, sequence=?,
|
||||
updated_at=CURRENT_TIMESTAMP WHERE id=?`).run(
|
||||
fields.title ?? existing.title,
|
||||
@@ -347,7 +335,6 @@ export function deletePoi(db, routeId, poiId) {
|
||||
const existing = db.prepare(`SELECT p.id, p.audio_path FROM pois p JOIN routes r ON r.id = p.route_id
|
||||
WHERE p.id = ? AND p.route_id = ? AND r.status = 'active'`).get(poiId, routeId);
|
||||
if (!existing) throw new HttpError(404, 'POI nicht gefunden.');
|
||||
|
||||
const images = db.prepare('SELECT path FROM poi_images WHERE poi_id = ?').all(poiId);
|
||||
transaction(db, () => {
|
||||
db.prepare('DELETE FROM poi_images WHERE poi_id = ?').run(poiId);
|
||||
@@ -362,7 +349,6 @@ export function deletePoi(db, routeId, poiId) {
|
||||
export function softDeleteRoute(db, id) {
|
||||
const route = db.prepare("SELECT id FROM routes WHERE id = ? AND status = 'active'").get(id);
|
||||
if (!route) throw new HttpError(404, 'Aktive Strecke nicht gefunden.');
|
||||
|
||||
const moved = softDeleteRouteDirectory(id);
|
||||
try {
|
||||
transaction(db, () => {
|
||||
@@ -375,14 +361,12 @@ export function softDeleteRoute(db, id) {
|
||||
fs.renameSync(moved.destination, routeDirectory(id));
|
||||
throw error;
|
||||
}
|
||||
|
||||
return rowToRoute(db.prepare('SELECT * FROM routes WHERE id = ?').get(id), true);
|
||||
}
|
||||
|
||||
export function restoreRoute(db, id) {
|
||||
const route = db.prepare("SELECT trash_path FROM routes WHERE id = ? AND status = 'deleted'").get(id);
|
||||
if (!route) throw new HttpError(404, 'Gelöschte Strecke nicht gefunden.');
|
||||
|
||||
const moved = restoreRouteDirectory(id, route.trash_path);
|
||||
try {
|
||||
transaction(db, () => {
|
||||
@@ -395,6 +379,5 @@ export function restoreRoute(db, id) {
|
||||
fs.renameSync(moved.destination, moved.source);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return getRoute(db, id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user