pre updating version control
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
const express = require("express");
|
||||
const crypto = require("crypto");
|
||||
const db = require("../db/polygons");
|
||||
const { applyEntityBatchChanges } = require("../lib/entityBatch");
|
||||
const { getBBox } = require("../utils/bbox");
|
||||
|
||||
const router = express.Router();
|
||||
@@ -304,169 +305,10 @@ router.post("/batch", (req, res) => {
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
try {
|
||||
const tx = db.transaction((items) => {
|
||||
for (const change of items) {
|
||||
const hasAction = Object.prototype.hasOwnProperty.call(change || {}, "action");
|
||||
const action = normalizeBatchAction(hasAction ? change.action : change?.type);
|
||||
|
||||
if (!change || !action) {
|
||||
throw createValidationError("Invalid change entry");
|
||||
}
|
||||
|
||||
if (action === "create") {
|
||||
const feature = change.feature;
|
||||
if (!feature || !feature.properties || !feature.properties.id || !feature.geometry) {
|
||||
throw createValidationError("Invalid create payload");
|
||||
}
|
||||
|
||||
const temporalRange = normalizeTemporalRange(
|
||||
feature.properties.time_start,
|
||||
feature.properties.time_end
|
||||
);
|
||||
const entityPayload = extractEntityIdsFromPayload(feature.properties || {});
|
||||
const entityIds = entityPayload.entityIds;
|
||||
ensureGeometryHasEntities(entityIds);
|
||||
validateEntityIdsExist(entityIds);
|
||||
|
||||
const bbox = getBBox(feature.geometry);
|
||||
const geometryId = String(feature.properties.id);
|
||||
const bindingPayload = extractBindingFromPayload(feature.properties || {});
|
||||
const bindingIds = sanitizeBindingIdsForGeometry(bindingPayload.bindingIds, geometryId);
|
||||
const geometryType = resolveGeometryType(feature.properties?.type, entityIds);
|
||||
|
||||
db.prepare(`
|
||||
INSERT OR REPLACE INTO geometries (
|
||||
id, type, is_deleted, draw_geometry, binding, time_start, time_end,
|
||||
bbox_min_lng, bbox_min_lat, bbox_max_lng, bbox_max_lat,
|
||||
created_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE((
|
||||
SELECT created_at FROM geometries WHERE id = ?
|
||||
), ?), ?)
|
||||
`).run(
|
||||
geometryId,
|
||||
geometryType,
|
||||
JSON.stringify(feature.geometry),
|
||||
serializeBindingIds(bindingIds),
|
||||
temporalRange.timeStart,
|
||||
temporalRange.timeEnd,
|
||||
bbox.minLng,
|
||||
bbox.minLat,
|
||||
bbox.maxLng,
|
||||
bbox.maxLat,
|
||||
geometryId,
|
||||
now,
|
||||
now
|
||||
);
|
||||
|
||||
syncGeometryEntityLinks(geometryId, entityIds, now);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (action === "update") {
|
||||
const { id, geometry } = change;
|
||||
if (!id || !geometry) {
|
||||
throw createValidationError("Invalid update payload");
|
||||
}
|
||||
|
||||
const existing = db.prepare(`
|
||||
SELECT id, type, time_start, time_end, binding
|
||||
FROM geometries
|
||||
WHERE id = ?
|
||||
AND is_deleted = 0
|
||||
`).get(id);
|
||||
|
||||
if (!existing) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasTimeStart = Object.prototype.hasOwnProperty.call(change, "time_start");
|
||||
const hasTimeEnd = Object.prototype.hasOwnProperty.call(change, "time_end");
|
||||
const hasType = hasAction && Object.prototype.hasOwnProperty.call(change, "type");
|
||||
const temporalRange = normalizeTemporalRange(
|
||||
hasTimeStart ? change.time_start : existing.time_start,
|
||||
hasTimeEnd ? change.time_end : existing.time_end
|
||||
);
|
||||
const bindingPayload = extractBindingFromPayload(change);
|
||||
const nextBindingIds = sanitizeBindingIdsForGeometry(
|
||||
bindingPayload.provided
|
||||
? bindingPayload.bindingIds
|
||||
: parseBindingIds(existing.binding),
|
||||
id
|
||||
);
|
||||
|
||||
const entityPayload = extractEntityIdsFromPayload(change);
|
||||
const nextEntityIds = entityPayload.provided
|
||||
? entityPayload.entityIds
|
||||
: getLinkedEntityIdsByGeometryId(id);
|
||||
ensureGeometryHasEntities(nextEntityIds);
|
||||
validateEntityIdsExist(nextEntityIds);
|
||||
const nextType = hasType
|
||||
? resolveGeometryType(change.type, nextEntityIds, existing.type)
|
||||
: resolveGeometryType(null, nextEntityIds, existing.type);
|
||||
|
||||
const bbox = getBBox(geometry);
|
||||
|
||||
db.prepare(`
|
||||
UPDATE geometries
|
||||
SET type = ?,
|
||||
draw_geometry = ?,
|
||||
binding = ?,
|
||||
time_start = ?,
|
||||
time_end = ?,
|
||||
bbox_min_lng = ?,
|
||||
bbox_min_lat = ?,
|
||||
bbox_max_lng = ?,
|
||||
bbox_max_lat = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
AND is_deleted = 0
|
||||
`).run(
|
||||
nextType,
|
||||
JSON.stringify(geometry),
|
||||
serializeBindingIds(nextBindingIds),
|
||||
temporalRange.timeStart,
|
||||
temporalRange.timeEnd,
|
||||
bbox.minLng,
|
||||
bbox.minLat,
|
||||
bbox.maxLng,
|
||||
bbox.maxLat,
|
||||
now,
|
||||
id
|
||||
);
|
||||
|
||||
if (entityPayload.provided) {
|
||||
syncGeometryEntityLinks(id, nextEntityIds, now);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (action === "delete") {
|
||||
if (!change.id) {
|
||||
throw createValidationError("Invalid delete payload");
|
||||
}
|
||||
|
||||
const result = db.prepare(`
|
||||
UPDATE geometries
|
||||
SET is_deleted = 1,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
AND is_deleted = 0
|
||||
`).run(now, change.id);
|
||||
|
||||
if (result.changes) {
|
||||
db.prepare(`DELETE FROM entity_geometries WHERE geometry_id = ?`).run(change.id);
|
||||
removeBindingReferenceFromAll(change.id, now);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
throw createValidationError(`Unknown change type: ${String(action)}`);
|
||||
}
|
||||
applyGeometryBatchChanges(items, now);
|
||||
});
|
||||
|
||||
tx(changes);
|
||||
res.json({ success: true, applied: changes.length });
|
||||
} catch (err) {
|
||||
@@ -478,6 +320,209 @@ router.post("/batch", (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// =======================
|
||||
// Apply entities + geometries in one transaction
|
||||
// =======================
|
||||
router.post("/batch/combined", (req, res) => {
|
||||
const entityChangesRaw = req.body?.entity_changes;
|
||||
const geometryChangesRaw = req.body?.geometry_changes;
|
||||
|
||||
if (entityChangesRaw !== undefined && !Array.isArray(entityChangesRaw)) {
|
||||
return res.status(400).json({ error: "entity_changes must be an array" });
|
||||
}
|
||||
if (geometryChangesRaw !== undefined && !Array.isArray(geometryChangesRaw)) {
|
||||
return res.status(400).json({ error: "geometry_changes must be an array" });
|
||||
}
|
||||
|
||||
const entityChanges = entityChangesRaw || [];
|
||||
const geometryChanges = geometryChangesRaw || [];
|
||||
const now = new Date().toISOString();
|
||||
|
||||
try {
|
||||
const tx = db.transaction((entities, geometries) => {
|
||||
const entityResult = applyEntityBatchChanges(entities, { now });
|
||||
applyGeometryBatchChanges(geometries, now);
|
||||
return entityResult;
|
||||
});
|
||||
|
||||
const entityResult = tx(entityChanges, geometryChanges);
|
||||
res.json({
|
||||
success: true,
|
||||
applied: entityChanges.length + geometryChanges.length,
|
||||
entity_applied: entityChanges.length,
|
||||
geometry_applied: geometryChanges.length,
|
||||
created_entity_ids: entityResult.createdEntityIds,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.status) {
|
||||
return res.status(err.status).json({ error: err.message });
|
||||
}
|
||||
console.error("Combined batch apply error:", err);
|
||||
res.status(500).json({ error: "Combined batch apply failed" });
|
||||
}
|
||||
});
|
||||
|
||||
function applyGeometryBatchChanges(items, now) {
|
||||
for (const change of items) {
|
||||
const hasAction = Object.prototype.hasOwnProperty.call(change || {}, "action");
|
||||
const action = normalizeBatchAction(hasAction ? change.action : change?.type);
|
||||
|
||||
if (!change || !action) {
|
||||
throw createValidationError("Invalid change entry");
|
||||
}
|
||||
|
||||
if (action === "create") {
|
||||
const feature = change.feature;
|
||||
if (!feature || !feature.properties || !feature.properties.id || !feature.geometry) {
|
||||
throw createValidationError("Invalid create payload");
|
||||
}
|
||||
|
||||
const temporalRange = normalizeTemporalRange(
|
||||
feature.properties.time_start,
|
||||
feature.properties.time_end
|
||||
);
|
||||
const entityPayload = extractEntityIdsFromPayload(feature.properties || {});
|
||||
const entityIds = entityPayload.entityIds;
|
||||
ensureGeometryHasEntities(entityIds);
|
||||
validateEntityIdsExist(entityIds);
|
||||
|
||||
const bbox = getBBox(feature.geometry);
|
||||
const geometryId = String(feature.properties.id);
|
||||
const bindingPayload = extractBindingFromPayload(feature.properties || {});
|
||||
const bindingIds = sanitizeBindingIdsForGeometry(bindingPayload.bindingIds, geometryId);
|
||||
const geometryType = resolveGeometryType(feature.properties?.type, entityIds);
|
||||
|
||||
db.prepare(`
|
||||
INSERT OR REPLACE INTO geometries (
|
||||
id, type, is_deleted, draw_geometry, binding, time_start, time_end,
|
||||
bbox_min_lng, bbox_min_lat, bbox_max_lng, bbox_max_lat,
|
||||
created_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE((
|
||||
SELECT created_at FROM geometries WHERE id = ?
|
||||
), ?), ?)
|
||||
`).run(
|
||||
geometryId,
|
||||
geometryType,
|
||||
JSON.stringify(feature.geometry),
|
||||
serializeBindingIds(bindingIds),
|
||||
temporalRange.timeStart,
|
||||
temporalRange.timeEnd,
|
||||
bbox.minLng,
|
||||
bbox.minLat,
|
||||
bbox.maxLng,
|
||||
bbox.maxLat,
|
||||
geometryId,
|
||||
now,
|
||||
now
|
||||
);
|
||||
|
||||
syncGeometryEntityLinks(geometryId, entityIds, now);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (action === "update") {
|
||||
const { id, geometry } = change;
|
||||
if (!id || !geometry) {
|
||||
throw createValidationError("Invalid update payload");
|
||||
}
|
||||
|
||||
const existing = db.prepare(`
|
||||
SELECT id, type, time_start, time_end, binding
|
||||
FROM geometries
|
||||
WHERE id = ?
|
||||
AND is_deleted = 0
|
||||
`).get(id);
|
||||
|
||||
if (!existing) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasTimeStart = Object.prototype.hasOwnProperty.call(change, "time_start");
|
||||
const hasTimeEnd = Object.prototype.hasOwnProperty.call(change, "time_end");
|
||||
const hasType = hasAction && Object.prototype.hasOwnProperty.call(change, "type");
|
||||
const temporalRange = normalizeTemporalRange(
|
||||
hasTimeStart ? change.time_start : existing.time_start,
|
||||
hasTimeEnd ? change.time_end : existing.time_end
|
||||
);
|
||||
const bindingPayload = extractBindingFromPayload(change);
|
||||
const nextBindingIds = sanitizeBindingIdsForGeometry(
|
||||
bindingPayload.provided
|
||||
? bindingPayload.bindingIds
|
||||
: parseBindingIds(existing.binding),
|
||||
id
|
||||
);
|
||||
|
||||
const entityPayload = extractEntityIdsFromPayload(change);
|
||||
const nextEntityIds = entityPayload.provided
|
||||
? entityPayload.entityIds
|
||||
: getLinkedEntityIdsByGeometryId(id);
|
||||
ensureGeometryHasEntities(nextEntityIds);
|
||||
validateEntityIdsExist(nextEntityIds);
|
||||
const nextType = hasType
|
||||
? resolveGeometryType(change.type, nextEntityIds, existing.type)
|
||||
: resolveGeometryType(null, nextEntityIds, existing.type);
|
||||
|
||||
const bbox = getBBox(geometry);
|
||||
|
||||
db.prepare(`
|
||||
UPDATE geometries
|
||||
SET type = ?,
|
||||
draw_geometry = ?,
|
||||
binding = ?,
|
||||
time_start = ?,
|
||||
time_end = ?,
|
||||
bbox_min_lng = ?,
|
||||
bbox_min_lat = ?,
|
||||
bbox_max_lng = ?,
|
||||
bbox_max_lat = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
AND is_deleted = 0
|
||||
`).run(
|
||||
nextType,
|
||||
JSON.stringify(geometry),
|
||||
serializeBindingIds(nextBindingIds),
|
||||
temporalRange.timeStart,
|
||||
temporalRange.timeEnd,
|
||||
bbox.minLng,
|
||||
bbox.minLat,
|
||||
bbox.maxLng,
|
||||
bbox.maxLat,
|
||||
now,
|
||||
id
|
||||
);
|
||||
|
||||
if (entityPayload.provided) {
|
||||
syncGeometryEntityLinks(id, nextEntityIds, now);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (action === "delete") {
|
||||
if (!change.id) {
|
||||
throw createValidationError("Invalid delete payload");
|
||||
}
|
||||
|
||||
const result = db.prepare(`
|
||||
UPDATE geometries
|
||||
SET is_deleted = 1,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
AND is_deleted = 0
|
||||
`).run(now, change.id);
|
||||
|
||||
if (result.changes) {
|
||||
db.prepare(`DELETE FROM entity_geometries WHERE geometry_id = ?`).run(change.id);
|
||||
removeBindingReferenceFromAll(change.id, now);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
throw createValidationError(`Unknown change type: ${String(action)}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
|
||||
function buildFeatureFromRow(row, linkedEntities = []) {
|
||||
|
||||
Reference in New Issue
Block a user