simplify application and remove window references
Sonarqube Scanner / Build and analyze (push) Failing after 1m27s
Sonarqube Scanner / Build and analyze (push) Failing after 1m27s
This commit is contained in:
+230
-82
@@ -6,49 +6,152 @@ import { HttpError } from '../middleware/errors.js';
|
||||
import { parseGpx } from './gpx.js';
|
||||
import { distanceMeters, routeMetrics } from './geo.js';
|
||||
import {
|
||||
ensureRouteDirectories, moveUploadedFile, routeDirectory,
|
||||
softDeleteRouteDirectory, restoreRouteDirectory, resolveStoredFile, removeUpload, removeStoredFile
|
||||
ensureRouteDirectories,
|
||||
moveUploadedFile,
|
||||
routeDirectory,
|
||||
softDeleteRouteDirectory,
|
||||
restoreRouteDirectory,
|
||||
resolveStoredFile,
|
||||
removeUpload,
|
||||
removeStoredFile
|
||||
} from './storage.js';
|
||||
|
||||
const slugify = value => value.toLowerCase().normalize('NFKD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 80) || `route-${Date.now()}`;
|
||||
const INSERT_POINT_SQL = 'INSERT INTO route_points (route_id, sequence, lat, lon, elevation, recorded_at) VALUES (?, ?, ?, ?, ?, ?)';
|
||||
const UPDATE_METRICS_SQL = `UPDATE routes SET
|
||||
start_lat=?, start_lon=?, center_lat=?, center_lon=?, min_lat=?, min_lon=?, max_lat=?, max_lon=?,
|
||||
distance_m=?, elevation_gain_m=?, point_count=?, updated_at=CURRENT_TIMESTAMP WHERE id=?`;
|
||||
|
||||
function slugify(value) {
|
||||
const normalized = String(value).toLowerCase().normalize('NFKD');
|
||||
const withoutMarks = normalized.replaceAll(/[\u0300-\u036f]/g, '');
|
||||
const separated = withoutMarks.replaceAll(/[^a-z0-9]+/g, '-');
|
||||
const trimmed = separated.replaceAll(/(?:^-+|-+$)/g, '');
|
||||
return trimmed.slice(0, 80) || `route-${Date.now()}`;
|
||||
}
|
||||
|
||||
const routeGpxUrl = routeId => `/api/routes/${routeId}/gpx`;
|
||||
const pictureUrl = (routeId, poiId, pictureId) => `/api/routes/${routeId}/pois/${poiId}/pictures/${pictureId}`;
|
||||
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, name: row.name, description: row.description,
|
||||
schoolName: row.school_name, status: row.status,
|
||||
id: row.id,
|
||||
slug: row.slug,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
schoolName: row.school_name,
|
||||
status: row.status,
|
||||
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 ? null : { minLat: row.min_lat, minLon: row.min_lon, maxLat: row.max_lat, maxLon: row.max_lon },
|
||||
distanceM: row.distance_m, elevationGainM: row.elevation_gain_m, pointCount: row.point_count,
|
||||
bounds: row.min_lat == null
|
||||
? null
|
||||
: { minLat: row.min_lat, minLon: row.min_lon, maxLat: row.max_lat, maxLon: row.max_lon },
|
||||
distanceM: row.distance_m,
|
||||
elevationGainM: row.elevation_gain_m,
|
||||
pointCount: row.point_count,
|
||||
gpxUrl: row.gpx_path && row.status === 'active' ? routeGpxUrl(row.id) : null,
|
||||
createdAt: row.created_at, updatedAt: row.updated_at, deletedAt: row.deleted_at
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
deletedAt: row.deleted_at
|
||||
};
|
||||
}
|
||||
|
||||
export function listRoutes(db, { lat, lon, radiusKm = config.defaultRouteRadiusKm, includeDeleted = false }) {
|
||||
const rows = db.prepare(`SELECT * FROM routes ${includeDeleted ? '' : "WHERE status = 'active'"} ORDER BY name COLLATE NOCASE`).all();
|
||||
function routeComparator(origin) {
|
||||
if (origin) return (left, right) => left.proximityM - right.proximityM;
|
||||
return (left, right) => left.name.localeCompare(right.name, 'de');
|
||||
}
|
||||
|
||||
function insertRoutePoints(db, routeId, points, offset = 0) {
|
||||
const insert = db.prepare(INSERT_POINT_SQL);
|
||||
points.forEach((point, index) => {
|
||||
insert.run(routeId, offset + index, point.lat, point.lon, point.elevation, point.recordedAt);
|
||||
});
|
||||
}
|
||||
|
||||
function updateMetrics(db, routeId, metrics) {
|
||||
db.prepare(UPDATE_METRICS_SQL).run(
|
||||
metrics.startLat,
|
||||
metrics.startLon,
|
||||
metrics.centerLat,
|
||||
metrics.centerLon,
|
||||
metrics.minLat,
|
||||
metrics.minLon,
|
||||
metrics.maxLat,
|
||||
metrics.maxLon,
|
||||
metrics.distanceM,
|
||||
metrics.elevationGainM,
|
||||
metrics.pointCount,
|
||||
routeId
|
||||
);
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
function rewriteMediaPaths(db, routeId, sourcePrefix, targetPrefix) {
|
||||
db.prepare('UPDATE pois SET audio_path = REPLACE(audio_path, ?, ?) WHERE route_id = ? AND audio_path IS NOT NULL')
|
||||
.run(sourcePrefix, targetPrefix, routeId);
|
||||
db.prepare(`UPDATE poi_images SET path = REPLACE(path, ?, ?)
|
||||
WHERE poi_id IN (SELECT id FROM pois WHERE route_id = ?)`)
|
||||
.run(sourcePrefix, targetPrefix, routeId);
|
||||
}
|
||||
|
||||
export function listRoutes(db, {
|
||||
lat,
|
||||
lon,
|
||||
radiusKm = config.defaultRouteRadiusKm,
|
||||
includeDeleted = false
|
||||
}) {
|
||||
const where = includeDeleted ? '' : "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;
|
||||
return rows.map(row => {
|
||||
const route = rowToRoute(row, includeDeleted);
|
||||
const proximityM = origin && route.start ? distanceMeters(origin, route.start) : null;
|
||||
return { ...route, proximityM };
|
||||
}).filter(route => !origin || route.proximityM <= radiusKm * 1000).sort((a, b) => origin ? a.proximityM - b.proximityM : a.name.localeCompare(b.name, 'de'));
|
||||
const maximumDistance = radiusKm * 1000;
|
||||
|
||||
return rows
|
||||
.map(row => {
|
||||
const route = rowToRoute(row, includeDeleted);
|
||||
const proximityM = origin && route?.start ? distanceMeters(origin, route.start) : null;
|
||||
return { ...route, proximityM };
|
||||
})
|
||||
.filter(route => !origin || route.proximityM <= maximumDistance)
|
||||
.sort(routeComparator(origin));
|
||||
}
|
||||
|
||||
export function getRoute(db, id, includeDeleted = false) {
|
||||
const route = rowToRoute(db.prepare('SELECT * FROM routes WHERE id = ?').get(id), includeDeleted);
|
||||
const row = db.prepare('SELECT * FROM routes WHERE id = ?').get(id);
|
||||
const route = rowToRoute(row, includeDeleted);
|
||||
if (!route) throw new HttpError(404, 'Strecke nicht gefunden.');
|
||||
const points = db.prepare('SELECT sequence, lat, lon, elevation, recorded_at AS recordedAt FROM route_points WHERE route_id = ? ORDER BY sequence').all(id);
|
||||
const pois = listPois(db, id);
|
||||
return { ...route, points, pois };
|
||||
|
||||
const points = db.prepare(`SELECT sequence, lat, lon, elevation, recorded_at AS recordedAt
|
||||
FROM route_points WHERE route_id = ? ORDER BY sequence`).all(id);
|
||||
return { ...route, points, pois: listPois(db, id) };
|
||||
}
|
||||
|
||||
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`
|
||||
@@ -57,38 +160,49 @@ 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, () => {
|
||||
let slug = slugify(fields.slug || name);
|
||||
let suffix = 2;
|
||||
while (db.prepare('SELECT 1 FROM routes WHERE slug = ?').get(slug)) slug = `${slugify(fields.slug || name)}-${suffix++}`;
|
||||
const slug = uniqueSlug(db, fields.slug || name);
|
||||
const metrics = parsed.metrics;
|
||||
const result = db.prepare(`INSERT INTO routes
|
||||
(slug, name, description, school_name, gpx_path, start_lat, start_lon, center_lat, center_lon, min_lat, min_lon, max_lat, max_lon, distance_m, elevation_gain_m, point_count)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
||||
.run(slug, name, fields.description || '', fields.schoolName || '', 'pending', parsed.metrics.startLat, parsed.metrics.startLon,
|
||||
parsed.metrics.centerLat, parsed.metrics.centerLon, parsed.metrics.minLat, parsed.metrics.minLon, parsed.metrics.maxLat, parsed.metrics.maxLon,
|
||||
parsed.metrics.distanceM, parsed.metrics.elevationGainM, parsed.metrics.pointCount);
|
||||
(slug, name, description, school_name, gpx_path, start_lat, start_lon, center_lat, center_lon,
|
||||
min_lat, min_lon, max_lat, max_lon, distance_m, elevation_gain_m, point_count)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
||||
slug,
|
||||
name,
|
||||
fields.description || '',
|
||||
fields.schoolName || '',
|
||||
'pending',
|
||||
metrics.startLat,
|
||||
metrics.startLon,
|
||||
metrics.centerLat,
|
||||
metrics.centerLon,
|
||||
metrics.minLat,
|
||||
metrics.minLon,
|
||||
metrics.maxLat,
|
||||
metrics.maxLon,
|
||||
metrics.distanceM,
|
||||
metrics.elevationGainM,
|
||||
metrics.pointCount
|
||||
);
|
||||
const id = Number(result.lastInsertRowid);
|
||||
const insertPoint = db.prepare('INSERT INTO route_points (route_id, sequence, lat, lon, elevation, recorded_at) VALUES (?, ?, ?, ?, ?, ?)');
|
||||
for (const point of parsed.points) insertPoint.run(id, point.sequence, point.lat, point.lon, point.elevation, point.recordedAt);
|
||||
insertRoutePoints(db, id, parsed.points);
|
||||
return id;
|
||||
});
|
||||
|
||||
ensureRouteDirectories(routeId);
|
||||
const finalGpx = path.join(routeDirectory(routeId), 'route.gpx');
|
||||
const gpxPath = moveUploadedFile(gpxFile, finalGpx);
|
||||
db.prepare("UPDATE routes SET gpx_path = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(gpxPath, routeId);
|
||||
const gpxPath = moveUploadedFile(gpxFile, path.join(routeDirectory(routeId), 'route.gpx'));
|
||||
db.prepare('UPDATE routes SET gpx_path = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
|
||||
.run(gpxPath, routeId);
|
||||
return getRoute(db, routeId);
|
||||
} catch (error) {
|
||||
removeUpload(gpxFile);
|
||||
if (routeId) {
|
||||
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 });
|
||||
}
|
||||
cleanupFailedRoute(db, routeId, gpxFile);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -96,52 +210,63 @@ 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.');
|
||||
let parsed = null;
|
||||
if (gpxFile) parsed = parseGpx(fs.readFileSync(gpxFile.path, 'utf8'));
|
||||
|
||||
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 WHERE id = ?`)
|
||||
.run(fields.name ?? existing.name, fields.description ?? existing.description, fields.schoolName ?? existing.school_name, id);
|
||||
if (parsed) {
|
||||
db.prepare('DELETE FROM route_points WHERE route_id = ?').run(id);
|
||||
const insertPoint = db.prepare('INSERT INTO route_points (route_id, sequence, lat, lon, elevation, recorded_at) VALUES (?, ?, ?, ?, ?, ?)');
|
||||
for (const point of parsed.points) insertPoint.run(id, point.sequence, point.lat, point.lon, point.elevation, point.recordedAt);
|
||||
const m = parsed.metrics;
|
||||
db.prepare(`UPDATE routes SET start_lat=?, start_lon=?, center_lat=?, center_lon=?, min_lat=?, min_lon=?, max_lat=?, max_lon=?, distance_m=?, elevation_gain_m=?, point_count=? WHERE id=?`)
|
||||
.run(m.startLat, m.startLon, m.centerLat, m.centerLon, m.minLat, m.minLon, m.maxLat, m.maxLon, m.distanceM, m.elevationGainM, m.pointCount, id);
|
||||
}
|
||||
db.prepare(`UPDATE routes SET name = ?, description = ?, school_name = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`).run(
|
||||
fields.name ?? existing.name,
|
||||
fields.description ?? existing.description,
|
||||
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);
|
||||
updateMetrics(db, id, parsed.metrics);
|
||||
});
|
||||
|
||||
if (gpxFile) moveUploadedFile(gpxFile, path.join(routeDirectory(id), 'route.gpx'));
|
||||
return getRoute(db, id);
|
||||
}
|
||||
|
||||
export function appendRoute(db, id, gpxFile) {
|
||||
if (!gpxFile) throw new HttpError(400, 'Eine GPX-Datei ist erforderlich.');
|
||||
const existing = db.prepare("SELECT * FROM routes WHERE id = ? AND status = 'active'").get(id);
|
||||
|
||||
const existing = db.prepare("SELECT id FROM routes WHERE id = ? AND status = 'active'").get(id);
|
||||
if (!existing) throw new HttpError(404, 'Strecke nicht gefunden.');
|
||||
|
||||
const parsed = parseGpx(fs.readFileSync(gpxFile.path, 'utf8'));
|
||||
const current = db.prepare('SELECT lat, lon, elevation, recorded_at AS recordedAt FROM route_points WHERE route_id = ? ORDER BY sequence').all(id);
|
||||
const all = current.concat(parsed.points).map((point, sequence) => ({ ...point, sequence }));
|
||||
const m = routeMetrics(all);
|
||||
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, () => {
|
||||
const insert = db.prepare('INSERT INTO route_points (route_id, sequence, lat, lon, elevation, recorded_at) VALUES (?, ?, ?, ?, ?, ?)');
|
||||
for (let i = 0; i < parsed.points.length; i += 1) {
|
||||
const p = parsed.points[i]; insert.run(id, current.length + i, p.lat, p.lon, p.elevation, p.recordedAt);
|
||||
}
|
||||
db.prepare(`UPDATE routes SET start_lat=?, start_lon=?, center_lat=?, center_lon=?, min_lat=?, min_lon=?, max_lat=?, max_lon=?, distance_m=?, elevation_gain_m=?, point_count=?, updated_at=CURRENT_TIMESTAMP WHERE id=?`)
|
||||
.run(m.startLat, m.startLon, m.centerLat, m.centerLon, m.minLat, m.minLon, m.maxLat, m.maxLon, m.distanceM, m.elevationGainM, m.pointCount, id);
|
||||
insertRoutePoints(db, id, parsed.points, current.length);
|
||||
updateMetrics(db, id, routeMetrics(allPoints));
|
||||
});
|
||||
|
||||
removeUpload(gpxFile);
|
||||
return getRoute(db, id);
|
||||
}
|
||||
|
||||
export function listPois(db, routeId) {
|
||||
const pois = db.prepare('SELECT * FROM pois WHERE route_id = ? ORDER BY sequence, id').all(routeId);
|
||||
const imageStatement = db.prepare('SELECT id, path, caption, sequence FROM poi_images WHERE poi_id = ? ORDER BY sequence, id');
|
||||
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, title: poi.title, description: poi.description,
|
||||
lat: poi.lat, lon: poi.lon, triggerRadiusM: poi.trigger_radius_m, sequence: poi.sequence,
|
||||
id: poi.id,
|
||||
routeId: poi.route_id,
|
||||
title: poi.title,
|
||||
description: poi.description,
|
||||
lat: poi.lat,
|
||||
lon: poi.lon,
|
||||
triggerRadiusM: poi.trigger_radius_m,
|
||||
sequence: poi.sequence,
|
||||
audioUrl: poi.audio_path ? audioUrl(poi.route_id, poi.id) : null,
|
||||
images: imageStatement.all(poi.id).map(image => ({
|
||||
images: images.all(poi.id).map(image => ({
|
||||
id: image.id,
|
||||
caption: image.caption,
|
||||
sequence: image.sequence,
|
||||
@@ -160,30 +285,51 @@ export function getPoi(db, routeId, poiId) {
|
||||
export function createPoi(db, routeId, fields) {
|
||||
const route = db.prepare("SELECT id FROM routes WHERE id = ? AND status = 'active'").get(routeId);
|
||||
if (!route) throw new HttpError(404, 'Strecke nicht gefunden.');
|
||||
|
||||
const lat = Number.parseFloat(fields.lat);
|
||||
const lon = Number.parseFloat(fields.lon);
|
||||
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, fields.title || 'Unbenannter POI', fields.description || '', lat, lon,
|
||||
Number.parseFloat(fields.triggerRadiusM || config.defaultPoiTriggerMeters), Number.parseInt(fields.sequence || '0', 10));
|
||||
|
||||
const result = db.prepare(`INSERT INTO pois
|
||||
(route_id, title, description, lat, lon, trigger_radius_m, sequence) VALUES (?, ?, ?, ?, ?, ?, ?)`).run(
|
||||
routeId,
|
||||
fields.title || 'Unbenannter POI',
|
||||
fields.description || '',
|
||||
lat,
|
||||
lon,
|
||||
Number.parseFloat(fields.triggerRadiusM || config.defaultPoiTriggerMeters),
|
||||
Number.parseInt(fields.sequence || '0', 10)
|
||||
);
|
||||
return listPois(db, routeId).find(poi => poi.id === Number(result.lastInsertRowid));
|
||||
}
|
||||
|
||||
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 || existing.status !== 'active') throw new HttpError(404, 'POI nicht gefunden.');
|
||||
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 ? existing.trigger_radius_m : Number.parseFloat(fields.triggerRadiusM);
|
||||
const radius = fields.triggerRadiusM == null
|
||||
? existing.trigger_radius_m
|
||||
: Number.parseFloat(fields.triggerRadiusM);
|
||||
const sequence = fields.sequence == null ? existing.sequence : Number.parseInt(fields.sequence, 10);
|
||||
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, fields.description ?? existing.description, lat, lon, radius, sequence, poiId);
|
||||
|
||||
db.prepare(`UPDATE pois SET title=?, description=?, lat=?, lon=?, trigger_radius_m=?, sequence=?,
|
||||
updated_at=CURRENT_TIMESTAMP WHERE id=?`).run(
|
||||
fields.title ?? existing.title,
|
||||
fields.description ?? existing.description,
|
||||
lat,
|
||||
lon,
|
||||
radius,
|
||||
sequence,
|
||||
poiId
|
||||
);
|
||||
return listPois(db, existing.route_id).find(poi => poi.id === Number(poiId));
|
||||
}
|
||||
|
||||
@@ -198,45 +344,47 @@ export function deletePoi(db, routeId, poiId) {
|
||||
db.prepare('DELETE FROM pois WHERE id = ? AND route_id = ?').run(poiId, routeId);
|
||||
});
|
||||
|
||||
for (const image of images) removeStoredFile(image.path);
|
||||
images.forEach(image => removeStoredFile(image.path));
|
||||
removeStoredFile(existing.audio_path);
|
||||
return { id: poiId, routeId, deleted: true };
|
||||
}
|
||||
|
||||
export function softDeleteRoute(db, id) {
|
||||
const route = db.prepare("SELECT * FROM routes WHERE id = ? AND status = 'active'").get(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, () => {
|
||||
db.prepare(`UPDATE routes SET status='deleted', deleted_at=CURRENT_TIMESTAMP, trash_path=?, gpx_path=REPLACE(gpx_path, ?, ?), updated_at=CURRENT_TIMESTAMP WHERE id=?`)
|
||||
db.prepare(`UPDATE routes SET status='deleted', deleted_at=CURRENT_TIMESTAMP, trash_path=?,
|
||||
gpx_path=REPLACE(gpx_path, ?, ?), updated_at=CURRENT_TIMESTAMP WHERE id=?`)
|
||||
.run(moved.targetPrefix, moved.sourcePrefix, moved.targetPrefix, id);
|
||||
db.prepare('UPDATE pois SET audio_path = REPLACE(audio_path, ?, ?) WHERE route_id = ? AND audio_path IS NOT NULL').run(moved.sourcePrefix, moved.targetPrefix, id);
|
||||
db.prepare(`UPDATE poi_images SET path = REPLACE(path, ?, ?) WHERE poi_id IN (SELECT id FROM pois WHERE route_id = ?)`)
|
||||
.run(moved.sourcePrefix, moved.targetPrefix, id);
|
||||
rewriteMediaPaths(db, id, moved.sourcePrefix, moved.targetPrefix);
|
||||
});
|
||||
} catch (error) {
|
||||
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 * FROM routes WHERE id = ? AND status = 'deleted'").get(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, () => {
|
||||
db.prepare(`UPDATE routes SET status='active', deleted_at=NULL, trash_path=NULL, gpx_path=REPLACE(gpx_path, ?, ?), updated_at=CURRENT_TIMESTAMP WHERE id=?`)
|
||||
.run(moved.sourcePrefix, moved.targetPrefix, id);
|
||||
db.prepare('UPDATE pois SET audio_path = REPLACE(audio_path, ?, ?) WHERE route_id = ? AND audio_path IS NOT NULL').run(moved.sourcePrefix, moved.targetPrefix, id);
|
||||
db.prepare(`UPDATE poi_images SET path = REPLACE(path, ?, ?) WHERE poi_id IN (SELECT id FROM pois WHERE route_id = ?)`)
|
||||
db.prepare(`UPDATE routes SET status='active', deleted_at=NULL, trash_path=NULL,
|
||||
gpx_path=REPLACE(gpx_path, ?, ?), updated_at=CURRENT_TIMESTAMP WHERE id=?`)
|
||||
.run(moved.sourcePrefix, moved.targetPrefix, id);
|
||||
rewriteMediaPaths(db, id, moved.sourcePrefix, moved.targetPrefix);
|
||||
});
|
||||
} catch (error) {
|
||||
fs.renameSync(moved.destination, moved.source);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return getRoute(db, id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user