reduce api | version control
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
const express = require("express");
|
||||
const crypto = require("crypto");
|
||||
const db = require("../db/polygons");
|
||||
const { applyEntityBatchChanges } = require("../lib/entityBatch");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -80,228 +78,6 @@ router.get("/:id", (req, res) => {
|
||||
res.json(normalizeEntityRow(row));
|
||||
});
|
||||
|
||||
router.post("/", (req, res) => {
|
||||
const name = normalizeRequiredString(req.body?.name);
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: "name is required" });
|
||||
}
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
const slug = normalizeOptionalString(req.body?.slug) || toSlug(name);
|
||||
const description = normalizeOptionalString(req.body?.description);
|
||||
const typeId = normalizeTypeId(req.body?.type_id);
|
||||
const status = normalizeOptionalNumber(req.body?.status) ?? 1;
|
||||
|
||||
try {
|
||||
db.prepare(`
|
||||
INSERT INTO entities (
|
||||
id, name, slug, description, type_id, status, is_deleted, created_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
name,
|
||||
slug,
|
||||
description,
|
||||
typeId,
|
||||
status,
|
||||
now,
|
||||
now
|
||||
);
|
||||
} catch (err) {
|
||||
if (isSqliteConstraint(err)) {
|
||||
return res.status(409).json({ error: "Entity name/slug must be unique" });
|
||||
}
|
||||
console.error("Create entity failed", err);
|
||||
return res.status(500).json({ error: "Create entity failed" });
|
||||
}
|
||||
|
||||
const created = db.prepare(`
|
||||
SELECT
|
||||
e.*,
|
||||
COUNT(eg.geometry_id) AS geometry_count
|
||||
FROM entities e
|
||||
LEFT JOIN entity_geometries eg
|
||||
ON eg.entity_id = e.id
|
||||
WHERE e.id = ?
|
||||
GROUP BY e.id
|
||||
`).get(id);
|
||||
|
||||
res.status(201).json(normalizeEntityRow(created));
|
||||
});
|
||||
|
||||
router.put("/:id", (req, res) => {
|
||||
const existing = db.prepare(`
|
||||
SELECT *
|
||||
FROM entities
|
||||
WHERE id = ?
|
||||
AND is_deleted = 0
|
||||
`).get(req.params.id);
|
||||
|
||||
if (!existing) {
|
||||
return res.status(404).json({ error: "Entity not found" });
|
||||
}
|
||||
|
||||
const hasName = Object.prototype.hasOwnProperty.call(req.body || {}, "name");
|
||||
const hasSlug = Object.prototype.hasOwnProperty.call(req.body || {}, "slug");
|
||||
const hasDescription = Object.prototype.hasOwnProperty.call(req.body || {}, "description");
|
||||
const hasTypeId = Object.prototype.hasOwnProperty.call(req.body || {}, "type_id");
|
||||
const hasStatus = Object.prototype.hasOwnProperty.call(req.body || {}, "status");
|
||||
|
||||
if (!hasName && !hasSlug && !hasDescription && !hasTypeId && !hasStatus) {
|
||||
return res.status(400).json({ error: "No updatable field provided" });
|
||||
}
|
||||
|
||||
const name = hasName ? normalizeRequiredString(req.body?.name) : existing.name;
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: "name cannot be empty" });
|
||||
}
|
||||
|
||||
const slug = hasSlug ? normalizeOptionalString(req.body?.slug) : existing.slug;
|
||||
const description = hasDescription ? normalizeOptionalString(req.body?.description) : existing.description;
|
||||
const typeId = hasTypeId ? normalizeTypeId(req.body?.type_id) : existing.type_id;
|
||||
const status = hasStatus
|
||||
? (normalizeOptionalNumber(req.body?.status) ?? existing.status)
|
||||
: existing.status;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
try {
|
||||
db.prepare(`
|
||||
UPDATE entities
|
||||
SET name = ?,
|
||||
slug = ?,
|
||||
description = ?,
|
||||
type_id = ?,
|
||||
status = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
name,
|
||||
slug,
|
||||
description,
|
||||
typeId,
|
||||
status,
|
||||
now,
|
||||
req.params.id
|
||||
);
|
||||
} catch (err) {
|
||||
if (isSqliteConstraint(err)) {
|
||||
return res.status(409).json({ error: "Entity name/slug must be unique" });
|
||||
}
|
||||
console.error("Update entity failed", err);
|
||||
return res.status(500).json({ error: "Update entity failed" });
|
||||
}
|
||||
|
||||
const updated = db.prepare(`
|
||||
SELECT
|
||||
e.*,
|
||||
COUNT(eg.geometry_id) AS geometry_count
|
||||
FROM entities e
|
||||
LEFT JOIN entity_geometries eg
|
||||
ON eg.entity_id = e.id
|
||||
WHERE e.id = ?
|
||||
AND e.is_deleted = 0
|
||||
GROUP BY e.id
|
||||
`).get(req.params.id);
|
||||
|
||||
res.json(normalizeEntityRow(updated));
|
||||
});
|
||||
|
||||
router.delete("/:id", (req, res) => {
|
||||
const entityId = req.params.id;
|
||||
const existing = db.prepare(`
|
||||
SELECT id
|
||||
FROM entities
|
||||
WHERE id = ?
|
||||
AND is_deleted = 0
|
||||
`).get(entityId);
|
||||
|
||||
if (!existing) {
|
||||
return res.status(404).json({ error: "Entity not found" });
|
||||
}
|
||||
|
||||
const orphanedGeometryRows = db.prepare(`
|
||||
SELECT eg_target.geometry_id
|
||||
FROM entity_geometries eg_target
|
||||
JOIN geometries g_target
|
||||
ON g_target.id = eg_target.geometry_id
|
||||
AND g_target.is_deleted = 0
|
||||
LEFT JOIN entity_geometries eg_other
|
||||
ON eg_other.geometry_id = eg_target.geometry_id
|
||||
AND eg_other.entity_id <> eg_target.entity_id
|
||||
LEFT JOIN entities e_other
|
||||
ON e_other.id = eg_other.entity_id
|
||||
AND e_other.is_deleted = 0
|
||||
WHERE eg_target.entity_id = ?
|
||||
GROUP BY eg_target.geometry_id
|
||||
HAVING COUNT(e_other.id) = 0
|
||||
`).all(entityId);
|
||||
|
||||
if (orphanedGeometryRows.length) {
|
||||
const previewIds = orphanedGeometryRows.slice(0, 10).map((row) => row.geometry_id);
|
||||
const suffix = orphanedGeometryRows.length > 10 ? ", ..." : "";
|
||||
return res.status(409).json({
|
||||
error: `Cannot delete entity. Reassign ${orphanedGeometryRows.length} linked geometries first: ${previewIds.join(", ")}${suffix}`,
|
||||
});
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const deleted = db.transaction((id, timestamp) => {
|
||||
const result = db.prepare(`
|
||||
UPDATE entities
|
||||
SET is_deleted = 1,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
AND is_deleted = 0
|
||||
`).run(timestamp, id);
|
||||
|
||||
if (!result.changes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
DELETE FROM entity_geometries
|
||||
WHERE entity_id = ?
|
||||
`).run(id);
|
||||
|
||||
return true;
|
||||
})(entityId, now);
|
||||
|
||||
if (!deleted) {
|
||||
return res.status(404).json({ error: "Entity not found" });
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
router.post("/batch", (req, res) => {
|
||||
const { changes } = req.body || {};
|
||||
if (!Array.isArray(changes)) {
|
||||
return res.status(400).json({ error: "changes must be an array" });
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
try {
|
||||
const tx = db.transaction((items) => applyEntityBatchChanges(items, { now }));
|
||||
const result = tx(changes);
|
||||
res.json({
|
||||
success: true,
|
||||
applied: result.applied,
|
||||
created_entity_ids: result.createdEntityIds,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.status) {
|
||||
return res.status(err.status).json({ error: err.message });
|
||||
}
|
||||
if (isSqliteConstraint(err)) {
|
||||
return res.status(409).json({ error: "Entity name/slug must be unique" });
|
||||
}
|
||||
console.error("Batch entity apply failed", err);
|
||||
res.status(500).json({ error: "Batch entity apply failed" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
function normalizeEntityRow(row) {
|
||||
@@ -318,34 +94,6 @@ function normalizeEntityRow(row) {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRequiredString(value) {
|
||||
if (typeof value !== "string") return null;
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length ? trimmed : null;
|
||||
}
|
||||
|
||||
function normalizeOptionalString(value) {
|
||||
if (value === undefined || value === null) return null;
|
||||
if (typeof value !== "string") return null;
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length ? trimmed : null;
|
||||
}
|
||||
|
||||
function normalizeOptionalNumber(value) {
|
||||
if (value === undefined || value === null || value === "") return null;
|
||||
const num = Number(value);
|
||||
if (!Number.isFinite(num)) return null;
|
||||
return num;
|
||||
}
|
||||
|
||||
function normalizeTypeId(value) {
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return "country";
|
||||
}
|
||||
const trimmed = String(value).trim().toLowerCase();
|
||||
return trimmed || "country";
|
||||
}
|
||||
|
||||
function normalizeLimit(value, fallback = 25, max = 100) {
|
||||
const num = Number(value);
|
||||
if (!Number.isFinite(num)) return fallback;
|
||||
@@ -353,18 +101,3 @@ function normalizeLimit(value, fallback = 25, max = 100) {
|
||||
if (intValue <= 0) return fallback;
|
||||
return Math.min(intValue, max);
|
||||
}
|
||||
|
||||
function toSlug(value) {
|
||||
return value
|
||||
.normalize("NFKD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.slice(0, 120) || null;
|
||||
}
|
||||
|
||||
function isSqliteConstraint(err) {
|
||||
const message = typeof err?.message === "string" ? err.message : "";
|
||||
return message.includes("UNIQUE constraint failed");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user