392 lines
12 KiB
C++
392 lines
12 KiB
C++
#include "storage/path_guide_store.hpp"
|
|
|
|
#include "storage/database.hpp"
|
|
#include "util/id_util.hpp"
|
|
#include "util/string_util.hpp"
|
|
|
|
#include <sqlite3.h>
|
|
|
|
#include <algorithm>
|
|
|
|
namespace lm {
|
|
|
|
namespace {
|
|
|
|
nlohmann::json parsePositionsJson(const std::string& raw)
|
|
{
|
|
if (raw.empty())
|
|
return nlohmann::json::array();
|
|
try
|
|
{
|
|
const auto parsed = nlohmann::json::parse(raw);
|
|
return parsed.is_array() ? parsed : nlohmann::json::array();
|
|
}
|
|
catch (...)
|
|
{
|
|
return nlohmann::json::array();
|
|
}
|
|
}
|
|
|
|
nlohmann::json rowToJson(sqlite3_stmt* stmt)
|
|
{
|
|
auto text = [&](int col) -> std::string {
|
|
if (sqlite3_column_type(stmt, col) == SQLITE_NULL)
|
|
return "";
|
|
const char* v = reinterpret_cast<const char*>(sqlite3_column_text(stmt, col));
|
|
return v ? std::string(v) : "";
|
|
};
|
|
|
|
const nlohmann::json positions = parsePositionsJson(text(4));
|
|
int starts_count = 0;
|
|
int vias_count = 0;
|
|
int goals_count = 0;
|
|
for (const auto& pos : positions)
|
|
{
|
|
if (!pos.is_object())
|
|
continue;
|
|
const std::string role = StringUtil::toLower(pos.value("role", ""));
|
|
if (role == "start")
|
|
++starts_count;
|
|
else if (role == "via")
|
|
++vias_count;
|
|
else if (role == "goal")
|
|
++goals_count;
|
|
}
|
|
|
|
return {{"id", text(0)},
|
|
{"site_id", text(1)},
|
|
{"map_id", text(2)},
|
|
{"name", text(3)},
|
|
{"positions", positions},
|
|
{"starts_count", starts_count},
|
|
{"vias_count", vias_count},
|
|
{"goals_count", goals_count},
|
|
{"created_by", text(5)},
|
|
{"created_by_group", text(6)},
|
|
{"created_at", text(7)},
|
|
{"updated_at", text(8)}};
|
|
}
|
|
|
|
constexpr const char* kSelect =
|
|
"SELECT id, site_id, map_id, name, positions_json, created_by, created_by_group, created_at, updated_at "
|
|
"FROM path_guides";
|
|
|
|
bool validRole(const std::string& role)
|
|
{
|
|
return role == "start" || role == "via" || role == "goal";
|
|
}
|
|
|
|
std::optional<nlohmann::json> normalizePositions(const nlohmann::json& raw, std::string& err)
|
|
{
|
|
if (!raw.is_array())
|
|
{
|
|
err = "positions must be an array";
|
|
return std::nullopt;
|
|
}
|
|
|
|
nlohmann::json out = nlohmann::json::array();
|
|
int starts = 0;
|
|
int goals = 0;
|
|
int via_priority = 0;
|
|
|
|
for (const auto& item : raw)
|
|
{
|
|
if (!item.is_object())
|
|
continue;
|
|
const std::string position_id = StringUtil::trimCopy(item.value("position_id", ""));
|
|
const std::string role = StringUtil::toLower(StringUtil::trimCopy(item.value("role", "")));
|
|
if (position_id.empty() || !validRole(role))
|
|
{
|
|
err = "invalid position entry";
|
|
return std::nullopt;
|
|
}
|
|
|
|
nlohmann::json entry = {{"position_id", position_id}, {"role", role}};
|
|
if (role == "via")
|
|
{
|
|
int priority = via_priority + 1;
|
|
if (item.contains("priority") && item["priority"].is_number_integer())
|
|
priority = item["priority"].get<int>();
|
|
if (priority <= 0)
|
|
priority = via_priority + 1;
|
|
via_priority = priority;
|
|
entry["priority"] = priority;
|
|
}
|
|
else if (role == "start")
|
|
{
|
|
++starts;
|
|
}
|
|
else if (role == "goal")
|
|
{
|
|
++goals;
|
|
}
|
|
out.push_back(std::move(entry));
|
|
}
|
|
|
|
if (starts < 1 || goals < 1)
|
|
{
|
|
err = "path guide requires at least one start and one goal";
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::sort(out.begin(), out.end(), [](const nlohmann::json& a, const nlohmann::json& b) {
|
|
const std::string ra = a.value("role", "");
|
|
const std::string rb = b.value("role", "");
|
|
if (ra != rb)
|
|
{
|
|
if (ra == "start")
|
|
return true;
|
|
if (rb == "start")
|
|
return false;
|
|
if (ra == "via")
|
|
return true;
|
|
return false;
|
|
}
|
|
if (ra == "via")
|
|
return a.value("priority", 0) < b.value("priority", 0);
|
|
return a.value("position_id", "") < b.value("position_id", "");
|
|
});
|
|
|
|
return out;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
PathGuideStore::PathGuideStore(Database& db) : db_(db) {}
|
|
|
|
bool PathGuideStore::findNameConflictUnlocked(const std::string& site_id,
|
|
const std::string& name,
|
|
const std::string& except_id) const
|
|
{
|
|
const std::string needle = StringUtil::toLower(StringUtil::trimCopy(name));
|
|
if (needle.empty())
|
|
return false;
|
|
|
|
sqlite3_stmt* stmt = nullptr;
|
|
const char* sql = except_id.empty()
|
|
? "SELECT id FROM path_guides WHERE site_id = ?1 AND lower(name) = lower(?2) LIMIT 1"
|
|
: "SELECT id FROM path_guides WHERE site_id = ?1 AND lower(name) = lower(?2) AND id != ?3 LIMIT 1";
|
|
if (sqlite3_prepare_v2(db_.handle(), sql, -1, &stmt, nullptr) != SQLITE_OK)
|
|
return false;
|
|
sqlite3_bind_text(stmt, 1, site_id.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, name.c_str(), -1, SQLITE_TRANSIENT);
|
|
if (!except_id.empty())
|
|
sqlite3_bind_text(stmt, 3, except_id.c_str(), -1, SQLITE_TRANSIENT);
|
|
const bool conflict = sqlite3_step(stmt) == SQLITE_ROW;
|
|
sqlite3_finalize(stmt);
|
|
return conflict;
|
|
}
|
|
|
|
nlohmann::json PathGuideStore::list(const std::string& site_id) const
|
|
{
|
|
std::lock_guard<std::mutex> lock(mu_);
|
|
nlohmann::json out = nlohmann::json::array();
|
|
std::string sql = kSelect;
|
|
if (!site_id.empty())
|
|
sql += " WHERE site_id = ?1";
|
|
sql += " ORDER BY site_id, map_id, name";
|
|
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db_.handle(), sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
|
|
return out;
|
|
if (!site_id.empty())
|
|
sqlite3_bind_text(stmt, 1, site_id.c_str(), -1, SQLITE_TRANSIENT);
|
|
while (sqlite3_step(stmt) == SQLITE_ROW)
|
|
out.push_back(rowToJson(stmt));
|
|
sqlite3_finalize(stmt);
|
|
return out;
|
|
}
|
|
|
|
std::optional<nlohmann::json> PathGuideStore::find(const std::string& id) const
|
|
{
|
|
std::lock_guard<std::mutex> lock(mu_);
|
|
const std::string sql = std::string(kSelect) + " WHERE id = ?1";
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db_.handle(), sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
|
|
return std::nullopt;
|
|
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
|
|
std::optional<nlohmann::json> out;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW)
|
|
out = rowToJson(stmt);
|
|
sqlite3_finalize(stmt);
|
|
return out;
|
|
}
|
|
|
|
std::optional<nlohmann::json> PathGuideStore::create(const nlohmann::json& payload, std::string& err)
|
|
{
|
|
if (!payload.is_object())
|
|
{
|
|
err = "payload must be an object";
|
|
return std::nullopt;
|
|
}
|
|
|
|
const std::string site_id = StringUtil::trimCopy(payload.value("site_id", ""));
|
|
const std::string map_id = StringUtil::trimCopy(payload.value("map_id", ""));
|
|
const std::string name = StringUtil::trimCopy(payload.value("name", ""));
|
|
const std::string created_by = StringUtil::trimCopy(payload.value("created_by", ""));
|
|
const std::string created_by_group = StringUtil::trimCopy(payload.value("created_by_group", ""));
|
|
const auto positions = normalizePositions(payload.value("positions", nlohmann::json::array()), err);
|
|
if (!positions)
|
|
return std::nullopt;
|
|
|
|
if (site_id.empty() || map_id.empty() || name.empty())
|
|
{
|
|
err = "missing required fields";
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(mu_);
|
|
if (findNameConflictUnlocked(site_id, name, ""))
|
|
{
|
|
err = "path guide name already exists for this site";
|
|
return std::nullopt;
|
|
}
|
|
|
|
const std::string id = payload.value("id", IdUtil::newId());
|
|
const std::string now = IdUtil::nowIso8601();
|
|
const std::string positions_json = positions->dump();
|
|
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db_.handle(),
|
|
"INSERT INTO path_guides(id, site_id, map_id, name, positions_json, created_by, "
|
|
"created_by_group, created_at, updated_at) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9)",
|
|
-1,
|
|
&stmt,
|
|
nullptr) != SQLITE_OK)
|
|
{
|
|
err = sqlite3_errmsg(db_.handle());
|
|
return std::nullopt;
|
|
}
|
|
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, site_id.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 3, map_id.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 4, name.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 5, positions_json.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 6, created_by.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 7, created_by_group.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 8, now.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 9, now.c_str(), -1, SQLITE_TRANSIENT);
|
|
if (sqlite3_step(stmt) != SQLITE_DONE)
|
|
{
|
|
err = sqlite3_errmsg(db_.handle());
|
|
sqlite3_finalize(stmt);
|
|
return std::nullopt;
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
|
|
nlohmann::json created = {{"id", id},
|
|
{"site_id", site_id},
|
|
{"map_id", map_id},
|
|
{"name", name},
|
|
{"positions", *positions},
|
|
{"created_by", created_by},
|
|
{"created_by_group", created_by_group},
|
|
{"created_at", now},
|
|
{"updated_at", now}};
|
|
int starts_count = 0;
|
|
int vias_count = 0;
|
|
int goals_count = 0;
|
|
for (const auto& pos : *positions)
|
|
{
|
|
const std::string role = pos.value("role", "");
|
|
if (role == "start")
|
|
++starts_count;
|
|
else if (role == "via")
|
|
++vias_count;
|
|
else if (role == "goal")
|
|
++goals_count;
|
|
}
|
|
created["starts_count"] = starts_count;
|
|
created["vias_count"] = vias_count;
|
|
created["goals_count"] = goals_count;
|
|
return created;
|
|
}
|
|
|
|
bool PathGuideStore::update(const std::string& id, const nlohmann::json& payload, std::string& err)
|
|
{
|
|
auto existing = find(id);
|
|
if (!existing)
|
|
{
|
|
err = "path guide not found";
|
|
return false;
|
|
}
|
|
|
|
nlohmann::json merged = *existing;
|
|
for (const char* key : {"site_id", "map_id", "name"})
|
|
{
|
|
if (payload.contains(key))
|
|
merged[key] = payload[key];
|
|
}
|
|
if (payload.contains("positions"))
|
|
merged["positions"] = payload["positions"];
|
|
|
|
const std::string site_id = StringUtil::trimCopy(merged.value("site_id", ""));
|
|
const std::string map_id = StringUtil::trimCopy(merged.value("map_id", ""));
|
|
const std::string name = StringUtil::trimCopy(merged.value("name", ""));
|
|
const auto positions = normalizePositions(merged.value("positions", nlohmann::json::array()), err);
|
|
if (!positions)
|
|
return false;
|
|
|
|
if (site_id.empty() || map_id.empty() || name.empty())
|
|
{
|
|
err = "missing required fields";
|
|
return false;
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(mu_);
|
|
if (findNameConflictUnlocked(site_id, name, id))
|
|
{
|
|
err = "path guide name already exists for this site";
|
|
return false;
|
|
}
|
|
|
|
const std::string now = IdUtil::nowIso8601();
|
|
const std::string positions_json = positions->dump();
|
|
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db_.handle(),
|
|
"UPDATE path_guides SET site_id=?2, map_id=?3, name=?4, positions_json=?5, updated_at=?6 WHERE id=?1",
|
|
-1,
|
|
&stmt,
|
|
nullptr) != SQLITE_OK)
|
|
{
|
|
err = sqlite3_errmsg(db_.handle());
|
|
return false;
|
|
}
|
|
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, site_id.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 3, map_id.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 4, name.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 5, positions_json.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 6, now.c_str(), -1, SQLITE_TRANSIENT);
|
|
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
|
|
if (!ok)
|
|
err = sqlite3_errmsg(db_.handle());
|
|
sqlite3_finalize(stmt);
|
|
return ok;
|
|
}
|
|
|
|
bool PathGuideStore::remove(const std::string& id, std::string& err)
|
|
{
|
|
if (!find(id))
|
|
{
|
|
err = "path guide not found";
|
|
return false;
|
|
}
|
|
std::lock_guard<std::mutex> lock(mu_);
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db_.handle(), "DELETE FROM path_guides WHERE id = ?1", -1, &stmt, nullptr) != SQLITE_OK)
|
|
{
|
|
err = sqlite3_errmsg(db_.handle());
|
|
return false;
|
|
}
|
|
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
|
|
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
|
|
if (!ok)
|
|
err = sqlite3_errmsg(db_.handle());
|
|
sqlite3_finalize(stmt);
|
|
return ok;
|
|
}
|
|
|
|
} // namespace lm
|