pre updating version control

This commit is contained in:
taDuc
2026-04-17 20:55:33 +07:00
parent 6f7e819aca
commit 5397bf9808
5 changed files with 806 additions and 166 deletions

View File

@@ -1,6 +1,7 @@
const express = require("express");
const crypto = require("crypto");
const db = require("../db/polygons");
const { applyEntityBatchChanges } = require("../lib/entityBatch");
const router = express.Router();
@@ -95,16 +96,15 @@ router.post("/", (req, res) => {
try {
db.prepare(`
INSERT INTO entities (
id, name, slug, description, type_id, kind, status, is_deleted, created_at, updated_at
id, name, slug, description, type_id, status, is_deleted, created_at, updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
`).run(
id,
name,
slug,
description,
typeId,
null,
status,
now,
now
@@ -173,7 +173,6 @@ router.put("/:id", (req, res) => {
slug = ?,
description = ?,
type_id = ?,
kind = ?,
status = ?,
updated_at = ?
WHERE id = ?
@@ -182,7 +181,6 @@ router.put("/:id", (req, res) => {
slug,
description,
typeId,
null,
status,
now,
req.params.id
@@ -277,6 +275,33 @@ router.delete("/:id", (req, res) => {
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) {

View File

@@ -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 = []) {