This commit is contained in:
@@ -78,6 +78,8 @@ CREATE TABLE IF NOT EXISTS sounds (
|
||||
file_name TEXT,
|
||||
duration_ms INTEGER,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
volume INTEGER NOT NULL DEFAULT 100,
|
||||
is_system INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
@@ -93,6 +95,19 @@ CREATE TABLE IF NOT EXISTS recordings (
|
||||
FOREIGN KEY (map_id) REFERENCES maps(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS transitions (
|
||||
id TEXT PRIMARY KEY,
|
||||
site_id TEXT NOT NULL,
|
||||
from_map_id TEXT NOT NULL,
|
||||
to_map_id TEXT NOT NULL,
|
||||
start_position_id TEXT NOT NULL,
|
||||
goal_position_id TEXT NOT NULL,
|
||||
mission_id TEXT NOT NULL,
|
||||
created_by TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dashboards (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
@@ -335,6 +350,51 @@ bool Database::applySchemaMigrations(std::string& err)
|
||||
setMeta("schema_version", "3");
|
||||
}
|
||||
|
||||
ver = getMeta("schema_version").value_or("1");
|
||||
if (ver == "3")
|
||||
{
|
||||
if (!tableHasColumn(db_, "sounds", "volume"))
|
||||
{
|
||||
if (!execSql(db_, "ALTER TABLE sounds ADD COLUMN volume INTEGER NOT NULL DEFAULT 100", err))
|
||||
return false;
|
||||
}
|
||||
if (!tableHasColumn(db_, "sounds", "is_system"))
|
||||
{
|
||||
if (!execSql(db_, "ALTER TABLE sounds ADD COLUMN is_system INTEGER NOT NULL DEFAULT 0", err))
|
||||
return false;
|
||||
}
|
||||
setMeta("schema_version", "4");
|
||||
}
|
||||
|
||||
ver = getMeta("schema_version").value_or("1");
|
||||
if (ver == "4")
|
||||
{
|
||||
if (!execSql(db_,
|
||||
"CREATE TABLE IF NOT EXISTS transitions ("
|
||||
"id TEXT PRIMARY KEY, "
|
||||
"site_id TEXT NOT NULL, "
|
||||
"from_map_id TEXT NOT NULL, "
|
||||
"to_map_id TEXT NOT NULL, "
|
||||
"start_position_id TEXT NOT NULL, "
|
||||
"goal_position_id TEXT NOT NULL, "
|
||||
"mission_id TEXT NOT NULL, "
|
||||
"created_by TEXT NOT NULL DEFAULT '', "
|
||||
"created_at TEXT NOT NULL, "
|
||||
"updated_at TEXT NOT NULL"
|
||||
")",
|
||||
err))
|
||||
return false;
|
||||
setMeta("schema_version", "5");
|
||||
}
|
||||
|
||||
ver = getMeta("schema_version").value_or("1");
|
||||
if (ver == "5")
|
||||
{
|
||||
if (!execSql(db_, "ALTER TABLE transitions ADD COLUMN created_by TEXT NOT NULL DEFAULT ''", err))
|
||||
return false;
|
||||
setMeta("schema_version", "6");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,21 @@
|
||||
#include "util/id_util.hpp"
|
||||
#include "util/string_util.hpp"
|
||||
|
||||
#include "util/string_util.hpp"
|
||||
|
||||
#include <sqlite3.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <vector>
|
||||
|
||||
namespace lm {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kSoundSelect =
|
||||
"SELECT id, name, description, file_name, duration_ms, enabled, volume, is_system, created_at, updated_at "
|
||||
"FROM sounds";
|
||||
|
||||
nlohmann::json rowToJson(sqlite3_stmt* stmt)
|
||||
{
|
||||
auto textOrNull = [&](int col) -> nlohmann::json {
|
||||
@@ -26,8 +35,44 @@ nlohmann::json rowToJson(sqlite3_stmt* stmt)
|
||||
? nlohmann::json(nullptr)
|
||||
: nlohmann::json(sqlite3_column_int(stmt, 4))},
|
||||
{"enabled", sqlite3_column_int(stmt, 5) != 0},
|
||||
{"created_at", textOrNull(6)},
|
||||
{"updated_at", textOrNull(7)}};
|
||||
{"volume", sqlite3_column_int(stmt, 6)},
|
||||
{"is_system", sqlite3_column_int(stmt, 7) != 0},
|
||||
{"created_at", textOrNull(8)},
|
||||
{"updated_at", textOrNull(9)}};
|
||||
}
|
||||
|
||||
int clampVolume(int v)
|
||||
{
|
||||
if (v < 0)
|
||||
return 0;
|
||||
if (v > 100)
|
||||
return 100;
|
||||
return v;
|
||||
}
|
||||
|
||||
bool isReservedSystemName(const std::string& name)
|
||||
{
|
||||
const std::string lower = StringUtil::toLower(StringUtil::trimCopy(name));
|
||||
return lower == "beep" || lower == "horn" || lower == "chime";
|
||||
}
|
||||
|
||||
bool findNameConflict(sqlite3* db, const std::string& name, const std::string& except_id)
|
||||
{
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db,
|
||||
except_id.empty()
|
||||
? "SELECT id FROM sounds WHERE lower(name) = lower(?1) LIMIT 1"
|
||||
: "SELECT id FROM sounds WHERE lower(name) = lower(?1) AND id != ?2 LIMIT 1",
|
||||
-1,
|
||||
&stmt,
|
||||
nullptr) != SQLITE_OK)
|
||||
return false;
|
||||
sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_TRANSIENT);
|
||||
if (!except_id.empty())
|
||||
sqlite3_bind_text(stmt, 2, except_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
const bool conflict = sqlite3_step(stmt) == SQLITE_ROW;
|
||||
sqlite3_finalize(stmt);
|
||||
return conflict;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -38,13 +83,9 @@ nlohmann::json SoundStore::list() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
nlohmann::json sounds = nlohmann::json::array();
|
||||
const std::string query = std::string(kSoundSelect) + " ORDER BY is_system DESC, name";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"SELECT id, name, description, file_name, duration_ms, enabled, created_at, updated_at "
|
||||
"FROM sounds ORDER BY name",
|
||||
-1,
|
||||
&stmt,
|
||||
nullptr) != SQLITE_OK)
|
||||
if (sqlite3_prepare_v2(db_.handle(), query.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
|
||||
return sounds;
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW)
|
||||
sounds.push_back(rowToJson(stmt));
|
||||
@@ -55,13 +96,9 @@ nlohmann::json SoundStore::list() const
|
||||
std::optional<nlohmann::json> SoundStore::find(const std::string& id) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
const std::string query = std::string(kSoundSelect) + " WHERE id = ?1";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"SELECT id, name, description, file_name, duration_ms, enabled, created_at, updated_at "
|
||||
"FROM sounds WHERE id = ?1",
|
||||
-1,
|
||||
&stmt,
|
||||
nullptr) != SQLITE_OK)
|
||||
if (sqlite3_prepare_v2(db_.handle(), query.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;
|
||||
@@ -84,17 +121,31 @@ std::optional<nlohmann::json> SoundStore::create(const nlohmann::json& payload,
|
||||
err = "name is required";
|
||||
return std::nullopt;
|
||||
}
|
||||
if (isReservedSystemName(name))
|
||||
{
|
||||
err = "name reserved for built-in system sounds";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
if (findNameConflict(db_.handle(), name, ""))
|
||||
{
|
||||
err = "sound name already exists";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const std::string id = payload.value("id", IdUtil::newId());
|
||||
const std::string now = IdUtil::nowIso8601();
|
||||
const std::string description = payload.value("description", "");
|
||||
const bool enabled = payload.value("enabled", true);
|
||||
const int volume = clampVolume(payload.value("volume", 100));
|
||||
const bool is_system = payload.value("is_system", false);
|
||||
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"INSERT INTO sounds(id, name, description, file_name, duration_ms, enabled, created_at, updated_at) "
|
||||
"VALUES(?1,?2,?3,NULL,NULL,?4,?5,?6)",
|
||||
"INSERT INTO sounds(id, name, description, file_name, duration_ms, enabled, volume, "
|
||||
"is_system, created_at, updated_at) "
|
||||
"VALUES(?1,?2,?3,NULL,NULL,?4,?5,?6,?7,?8)",
|
||||
-1,
|
||||
&stmt,
|
||||
nullptr) != SQLITE_OK)
|
||||
@@ -106,8 +157,10 @@ std::optional<nlohmann::json> SoundStore::create(const nlohmann::json& payload,
|
||||
sqlite3_bind_text(stmt, 2, name.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 3, description.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int(stmt, 4, enabled ? 1 : 0);
|
||||
sqlite3_bind_text(stmt, 5, now.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 6, now.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int(stmt, 5, volume);
|
||||
sqlite3_bind_int(stmt, 6, is_system ? 1 : 0);
|
||||
sqlite3_bind_text(stmt, 7, now.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 8, now.c_str(), -1, SQLITE_TRANSIENT);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE)
|
||||
{
|
||||
@@ -123,6 +176,8 @@ std::optional<nlohmann::json> SoundStore::create(const nlohmann::json& payload,
|
||||
{"file_name", nullptr},
|
||||
{"duration_ms", nullptr},
|
||||
{"enabled", enabled},
|
||||
{"volume", volume},
|
||||
{"is_system", is_system},
|
||||
{"created_at", now},
|
||||
{"updated_at", now}};
|
||||
}
|
||||
@@ -136,18 +191,42 @@ bool SoundStore::update(const std::string& id, const nlohmann::json& payload, st
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool is_system = existing->value("is_system", false);
|
||||
nlohmann::json merged = *existing;
|
||||
for (const char* key : {"name", "description", "enabled", "duration_ms"})
|
||||
for (const char* key : {"description", "enabled", "duration_ms", "volume"})
|
||||
{
|
||||
if (payload.contains(key))
|
||||
merged[key] = payload[key];
|
||||
}
|
||||
if (!is_system && payload.contains("name"))
|
||||
{
|
||||
const std::string new_name = StringUtil::trimCopy(payload["name"].get<std::string>());
|
||||
if (new_name.empty())
|
||||
{
|
||||
err = "name is required";
|
||||
return false;
|
||||
}
|
||||
if (isReservedSystemName(new_name))
|
||||
{
|
||||
err = "name reserved for built-in system sounds";
|
||||
return false;
|
||||
}
|
||||
if (findNameConflict(db_.handle(), new_name, id))
|
||||
{
|
||||
err = "sound name already exists";
|
||||
return false;
|
||||
}
|
||||
merged["name"] = new_name;
|
||||
}
|
||||
if (merged.contains("volume"))
|
||||
merged["volume"] = clampVolume(merged["volume"].get<int>());
|
||||
|
||||
const std::string now = IdUtil::nowIso8601();
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"UPDATE sounds SET name=?2, description=?3, enabled=?4, duration_ms=?5, updated_at=?6 WHERE id=?1",
|
||||
"UPDATE sounds SET name=?2, description=?3, enabled=?4, duration_ms=?5, volume=?6, "
|
||||
"updated_at=?7 WHERE id=?1",
|
||||
-1,
|
||||
&stmt,
|
||||
nullptr) != SQLITE_OK)
|
||||
@@ -163,7 +242,8 @@ bool SoundStore::update(const std::string& id, const nlohmann::json& payload, st
|
||||
sqlite3_bind_int(stmt, 5, merged["duration_ms"].get<int>());
|
||||
else
|
||||
sqlite3_bind_null(stmt, 5);
|
||||
sqlite3_bind_text(stmt, 6, now.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int(stmt, 6, clampVolume(merged.value("volume", 100)));
|
||||
sqlite3_bind_text(stmt, 7, now.c_str(), -1, SQLITE_TRANSIENT);
|
||||
|
||||
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
|
||||
if (!ok)
|
||||
@@ -180,6 +260,11 @@ bool SoundStore::remove(const std::string& id, std::string& err)
|
||||
err = "sound not found";
|
||||
return false;
|
||||
}
|
||||
if (existing->value("is_system", false))
|
||||
{
|
||||
err = "system sounds cannot be deleted";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
@@ -217,11 +302,17 @@ bool SoundStore::saveFile(const std::string& id,
|
||||
const std::string& bytes,
|
||||
std::string& err)
|
||||
{
|
||||
if (!find(id))
|
||||
auto existing = find(id);
|
||||
if (!existing)
|
||||
{
|
||||
err = "sound not found";
|
||||
return false;
|
||||
}
|
||||
if (existing->value("is_system", false))
|
||||
{
|
||||
err = "system sounds cannot be replaced";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(db_.soundsDir(), ec);
|
||||
@@ -254,4 +345,57 @@ bool SoundStore::saveFile(const std::string& id,
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool SoundStore::ensureSystemDefaults(std::string& err)
|
||||
{
|
||||
{
|
||||
std::vector<std::string> dup_ids;
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"SELECT id FROM sounds WHERE is_system = 0 AND lower(name) IN ('beep', 'horn', 'chime')",
|
||||
-1,
|
||||
&stmt,
|
||||
nullptr) == SQLITE_OK)
|
||||
{
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW)
|
||||
{
|
||||
if (sqlite3_column_type(stmt, 0) != SQLITE_NULL)
|
||||
dup_ids.push_back(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0)));
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
for (const auto& dup_id : dup_ids)
|
||||
{
|
||||
sqlite3_stmt* del = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(), "DELETE FROM sounds WHERE id = ?1", -1, &del, nullptr) == SQLITE_OK)
|
||||
{
|
||||
sqlite3_bind_text(del, 1, dup_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_step(del);
|
||||
sqlite3_finalize(del);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DefaultSound
|
||||
{
|
||||
const char* id;
|
||||
const char* name;
|
||||
};
|
||||
static const DefaultSound kDefaults[] = {
|
||||
{"sys_beep", "Beep"},
|
||||
{"sys_horn", "Horn"},
|
||||
{"sys_chime", "Chime"},
|
||||
};
|
||||
|
||||
for (const auto& d : kDefaults)
|
||||
{
|
||||
if (find(d.id))
|
||||
continue;
|
||||
nlohmann::json payload = {{"id", d.id}, {"name", d.name}, {"is_system", true}, {"volume", 100}};
|
||||
if (!create(payload, err))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace lm
|
||||
|
||||
@@ -24,6 +24,7 @@ public:
|
||||
|
||||
std::optional<std::filesystem::path> filePath(const std::string& id) const;
|
||||
bool saveFile(const std::string& id, const std::string& filename, const std::string& bytes, std::string& err);
|
||||
bool ensureSystemDefaults(std::string& err);
|
||||
|
||||
private:
|
||||
Database& db_;
|
||||
|
||||
258
src/storage/transition_store.cpp
Normal file
258
src/storage/transition_store.cpp
Normal file
@@ -0,0 +1,258 @@
|
||||
#include "storage/transition_store.hpp"
|
||||
|
||||
#include "storage/database.hpp"
|
||||
#include "util/id_util.hpp"
|
||||
#include "util/string_util.hpp"
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
namespace lm {
|
||||
|
||||
namespace {
|
||||
|
||||
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) : "";
|
||||
};
|
||||
return {{"id", text(0)},
|
||||
{"site_id", text(1)},
|
||||
{"from_map_id", text(2)},
|
||||
{"to_map_id", text(3)},
|
||||
{"start_position_id", text(4)},
|
||||
{"goal_position_id", text(5)},
|
||||
{"mission_id", text(6)},
|
||||
{"created_by", text(7)},
|
||||
{"created_at", text(8)},
|
||||
{"updated_at", text(9)}};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TransitionStore::TransitionStore(Database& db) : db_(db) {}
|
||||
|
||||
nlohmann::json TransitionStore::list(const std::string& site_id) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
nlohmann::json out = nlohmann::json::array();
|
||||
std::string sql =
|
||||
"SELECT id, site_id, from_map_id, to_map_id, start_position_id, goal_position_id, mission_id, created_by, created_at, updated_at "
|
||||
"FROM transitions";
|
||||
if (!site_id.empty())
|
||||
sql += " WHERE site_id = ?1";
|
||||
sql += " ORDER BY site_id, from_map_id, to_map_id, created_at";
|
||||
|
||||
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> TransitionStore::findBetween(const std::string& from_map_id,
|
||||
const std::string& to_map_id) const
|
||||
{
|
||||
if (from_map_id.empty() || to_map_id.empty() || from_map_id == to_map_id)
|
||||
return std::nullopt;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"SELECT id, site_id, from_map_id, to_map_id, start_position_id, goal_position_id, mission_id, "
|
||||
"created_by, created_at, updated_at FROM transitions WHERE from_map_id = ?1 AND to_map_id = ?2 LIMIT 1",
|
||||
-1,
|
||||
&stmt,
|
||||
nullptr) != SQLITE_OK)
|
||||
return std::nullopt;
|
||||
sqlite3_bind_text(stmt, 1, from_map_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, to_map_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> TransitionStore::find(const std::string& id) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"SELECT id, site_id, from_map_id, to_map_id, start_position_id, goal_position_id, mission_id, "
|
||||
"created_by, created_at, updated_at FROM transitions WHERE id = ?1",
|
||||
-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> TransitionStore::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 from_map_id = StringUtil::trimCopy(payload.value("from_map_id", ""));
|
||||
const std::string to_map_id = StringUtil::trimCopy(payload.value("to_map_id", ""));
|
||||
const std::string start_position_id = StringUtil::trimCopy(payload.value("start_position_id", ""));
|
||||
const std::string goal_position_id = StringUtil::trimCopy(payload.value("goal_position_id", ""));
|
||||
const std::string mission_id = StringUtil::trimCopy(payload.value("mission_id", ""));
|
||||
const std::string created_by = StringUtil::trimCopy(payload.value("created_by", ""));
|
||||
if (site_id.empty() || from_map_id.empty() || to_map_id.empty() || start_position_id.empty() || goal_position_id.empty() ||
|
||||
mission_id.empty())
|
||||
{
|
||||
err = "missing required fields";
|
||||
return std::nullopt;
|
||||
}
|
||||
if (from_map_id == to_map_id)
|
||||
{
|
||||
err = "from_map_id and to_map_id must differ";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const std::string id = payload.value("id", IdUtil::newId());
|
||||
const std::string now = IdUtil::nowIso8601();
|
||||
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"INSERT INTO transitions(id, site_id, from_map_id, to_map_id, start_position_id, goal_position_id, "
|
||||
"mission_id, created_by, created_at, updated_at) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10)",
|
||||
-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, from_map_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 4, to_map_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 5, start_position_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 6, goal_position_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 7, mission_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 8, created_by.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 9, now.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 10, 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);
|
||||
return nlohmann::json{{"id", id},
|
||||
{"site_id", site_id},
|
||||
{"from_map_id", from_map_id},
|
||||
{"to_map_id", to_map_id},
|
||||
{"start_position_id", start_position_id},
|
||||
{"goal_position_id", goal_position_id},
|
||||
{"mission_id", mission_id},
|
||||
{"created_by", created_by},
|
||||
{"created_at", now},
|
||||
{"updated_at", now}};
|
||||
}
|
||||
|
||||
bool TransitionStore::update(const std::string& id, const nlohmann::json& payload, std::string& err)
|
||||
{
|
||||
auto existing = find(id);
|
||||
if (!existing)
|
||||
{
|
||||
err = "transition not found";
|
||||
return false;
|
||||
}
|
||||
|
||||
nlohmann::json merged = *existing;
|
||||
for (const char* key :
|
||||
{"site_id", "from_map_id", "to_map_id", "start_position_id", "goal_position_id", "mission_id"})
|
||||
{
|
||||
if (payload.contains(key))
|
||||
merged[key] = payload[key];
|
||||
}
|
||||
|
||||
const std::string site_id = StringUtil::trimCopy(merged.value("site_id", ""));
|
||||
const std::string from_map_id = StringUtil::trimCopy(merged.value("from_map_id", ""));
|
||||
const std::string to_map_id = StringUtil::trimCopy(merged.value("to_map_id", ""));
|
||||
const std::string start_position_id = StringUtil::trimCopy(merged.value("start_position_id", ""));
|
||||
const std::string goal_position_id = StringUtil::trimCopy(merged.value("goal_position_id", ""));
|
||||
const std::string mission_id = StringUtil::trimCopy(merged.value("mission_id", ""));
|
||||
if (site_id.empty() || from_map_id.empty() || to_map_id.empty() || start_position_id.empty() || goal_position_id.empty() ||
|
||||
mission_id.empty())
|
||||
{
|
||||
err = "missing required fields";
|
||||
return false;
|
||||
}
|
||||
if (from_map_id == to_map_id)
|
||||
{
|
||||
err = "from_map_id and to_map_id must differ";
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string now = IdUtil::nowIso8601();
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"UPDATE transitions SET site_id=?2, from_map_id=?3, to_map_id=?4, start_position_id=?5, "
|
||||
"goal_position_id=?6, mission_id=?7, updated_at=?8 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, from_map_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 4, to_map_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 5, start_position_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 6, goal_position_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 7, mission_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 8, 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 TransitionStore::remove(const std::string& id, std::string& err)
|
||||
{
|
||||
if (!find(id))
|
||||
{
|
||||
err = "transition not found";
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(), "DELETE FROM transitions 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
|
||||
|
||||
32
src/storage/transition_store.hpp
Normal file
32
src/storage/transition_store.hpp
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace lm {
|
||||
|
||||
class Database;
|
||||
|
||||
class TransitionStore
|
||||
{
|
||||
public:
|
||||
explicit TransitionStore(Database& db);
|
||||
|
||||
nlohmann::json list(const std::string& site_id = "") const;
|
||||
std::optional<nlohmann::json> find(const std::string& id) const;
|
||||
std::optional<nlohmann::json> findBetween(const std::string& from_map_id,
|
||||
const std::string& to_map_id) const;
|
||||
std::optional<nlohmann::json> create(const nlohmann::json& payload, std::string& err);
|
||||
bool update(const std::string& id, const nlohmann::json& payload, std::string& err);
|
||||
bool remove(const std::string& id, std::string& err);
|
||||
|
||||
private:
|
||||
Database& db_;
|
||||
mutable std::mutex mu_;
|
||||
};
|
||||
|
||||
} // namespace lm
|
||||
|
||||
Reference in New Issue
Block a user