This commit is contained in:
@@ -42,10 +42,19 @@ CREATE TABLE IF NOT EXISTS layout_profiles (
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sites (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS maps (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
site_id TEXT,
|
||||
created_by TEXT NOT NULL DEFAULT '',
|
||||
width REAL,
|
||||
height REAL,
|
||||
resolution REAL,
|
||||
@@ -56,7 +65,8 @@ CREATE TABLE IF NOT EXISTS maps (
|
||||
yaml_file TEXT,
|
||||
zones_json TEXT NOT NULL DEFAULT '[]',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sounds (
|
||||
@@ -188,6 +198,86 @@ bool Database::applySchema(std::string& err)
|
||||
return execSql(db_, kSchemaSql, err);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
bool tableHasColumn(sqlite3* db, const char* table, const char* column)
|
||||
{
|
||||
std::string sql = "PRAGMA table_info(";
|
||||
sql += table;
|
||||
sql += ")";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
|
||||
return false;
|
||||
bool found = false;
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW)
|
||||
{
|
||||
const char* name = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
|
||||
if (name && column == std::string(name))
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
return found;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Database::applySchemaMigrations(std::string& err)
|
||||
{
|
||||
const std::string ver = getMeta("schema_version").value_or("1");
|
||||
|
||||
if (ver == "1")
|
||||
{
|
||||
if (!execSql(db_,
|
||||
"CREATE TABLE IF NOT EXISTS sites ("
|
||||
"id TEXT PRIMARY KEY, name TEXT NOT NULL, "
|
||||
"created_at TEXT NOT NULL, updated_at TEXT NOT NULL)",
|
||||
err))
|
||||
return false;
|
||||
|
||||
if (!tableHasColumn(db_, "maps", "site_id"))
|
||||
{
|
||||
if (!execSql(db_, "ALTER TABLE maps ADD COLUMN site_id TEXT", err))
|
||||
return false;
|
||||
}
|
||||
if (!tableHasColumn(db_, "maps", "created_by"))
|
||||
{
|
||||
if (!execSql(db_, "ALTER TABLE maps ADD COLUMN created_by TEXT NOT NULL DEFAULT ''", err))
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string now = IdUtil::nowIso8601();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_,
|
||||
"INSERT OR IGNORE INTO sites(id, name, created_at, updated_at) "
|
||||
"VALUES('site_configuration', 'ConfigurationSite', ?1, ?1)",
|
||||
-1,
|
||||
&stmt,
|
||||
nullptr) == SQLITE_OK)
|
||||
{
|
||||
sqlite3_bind_text(stmt, 1, now.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
if (sqlite3_prepare_v2(db_,
|
||||
"UPDATE maps SET site_id = 'site_configuration' WHERE site_id IS NULL OR site_id = ''",
|
||||
-1,
|
||||
&stmt,
|
||||
nullptr) == SQLITE_OK)
|
||||
{
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
setMeta("schema_version", "2");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<std::string> Database::getMeta(const std::string& key) const
|
||||
{
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
@@ -400,6 +490,8 @@ bool Database::init(std::string& err)
|
||||
return false;
|
||||
if (!applySchema(err))
|
||||
return false;
|
||||
if (!applySchemaMigrations(err))
|
||||
return false;
|
||||
if (!ensureDataDirs(err))
|
||||
return false;
|
||||
if (!migrateFromJsonIfNeeded(err))
|
||||
|
||||
@@ -42,6 +42,7 @@ private:
|
||||
|
||||
bool openDb(std::string& err);
|
||||
bool applySchema(std::string& err);
|
||||
bool applySchemaMigrations(std::string& err);
|
||||
bool migrateFromJsonIfNeeded(std::string& err);
|
||||
bool ensureDataDirs(std::string& err);
|
||||
std::optional<std::string> getMeta(const std::string& key) const;
|
||||
|
||||
@@ -11,6 +11,11 @@ namespace lm {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kMapSelect =
|
||||
"SELECT id, name, description, site_id, created_by, width, height, resolution, "
|
||||
"origin_x, origin_y, origin_yaw, image_file, yaml_file, zones_json, created_at, updated_at "
|
||||
"FROM maps";
|
||||
|
||||
nlohmann::json rowToJson(sqlite3_stmt* stmt)
|
||||
{
|
||||
auto textOrNull = [&](int col) -> nlohmann::json {
|
||||
@@ -25,11 +30,11 @@ nlohmann::json rowToJson(sqlite3_stmt* stmt)
|
||||
};
|
||||
|
||||
nlohmann::json zones = nlohmann::json::array();
|
||||
if (sqlite3_column_type(stmt, 11) != SQLITE_NULL)
|
||||
if (sqlite3_column_type(stmt, 13) != SQLITE_NULL)
|
||||
{
|
||||
try
|
||||
{
|
||||
zones = nlohmann::json::parse(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 11)));
|
||||
zones = nlohmann::json::parse(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 13)));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
@@ -40,17 +45,19 @@ nlohmann::json rowToJson(sqlite3_stmt* stmt)
|
||||
return {{"id", textOrNull(0)},
|
||||
{"name", textOrNull(1)},
|
||||
{"description", textOrNull(2)},
|
||||
{"width", realOrNull(3)},
|
||||
{"height", realOrNull(4)},
|
||||
{"resolution", realOrNull(5)},
|
||||
{"origin_x", realOrNull(6)},
|
||||
{"origin_y", realOrNull(7)},
|
||||
{"origin_yaw", realOrNull(8)},
|
||||
{"image_file", textOrNull(9)},
|
||||
{"yaml_file", textOrNull(10)},
|
||||
{"site_id", textOrNull(3)},
|
||||
{"created_by", textOrNull(4)},
|
||||
{"width", realOrNull(5)},
|
||||
{"height", realOrNull(6)},
|
||||
{"resolution", realOrNull(7)},
|
||||
{"origin_x", realOrNull(8)},
|
||||
{"origin_y", realOrNull(9)},
|
||||
{"origin_yaw", realOrNull(10)},
|
||||
{"image_file", textOrNull(11)},
|
||||
{"yaml_file", textOrNull(12)},
|
||||
{"zones", zones},
|
||||
{"created_at", textOrNull(12)},
|
||||
{"updated_at", textOrNull(13)}};
|
||||
{"created_at", textOrNull(14)},
|
||||
{"updated_at", textOrNull(15)}};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -66,14 +73,9 @@ nlohmann::json MapStore::list() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
nlohmann::json maps = nlohmann::json::array();
|
||||
std::string sql = std::string(kMapSelect) + " ORDER BY name";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"SELECT id, name, description, width, height, resolution, "
|
||||
"origin_x, origin_y, origin_yaw, image_file, yaml_file, zones_json, "
|
||||
"created_at, updated_at FROM maps ORDER BY name",
|
||||
-1,
|
||||
&stmt,
|
||||
nullptr) != SQLITE_OK)
|
||||
if (sqlite3_prepare_v2(db_.handle(), sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
|
||||
return maps;
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW)
|
||||
maps.push_back(rowToJson(stmt));
|
||||
@@ -84,14 +86,9 @@ nlohmann::json MapStore::list() const
|
||||
std::optional<nlohmann::json> MapStore::find(const std::string& id) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
std::string sql = std::string(kMapSelect) + " WHERE id = ?1";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"SELECT id, name, description, width, height, resolution, "
|
||||
"origin_x, origin_y, origin_yaw, image_file, yaml_file, zones_json, "
|
||||
"created_at, updated_at FROM maps WHERE id = ?1",
|
||||
-1,
|
||||
&stmt,
|
||||
nullptr) != SQLITE_OK)
|
||||
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;
|
||||
@@ -118,6 +115,8 @@ std::optional<nlohmann::json> MapStore::create(const nlohmann::json& payload, st
|
||||
const std::string id = payload.value("id", IdUtil::newId());
|
||||
const std::string now = IdUtil::nowIso8601();
|
||||
const std::string description = payload.value("description", "");
|
||||
const std::string site_id = payload.value("site_id", "");
|
||||
const std::string created_by = payload.value("created_by", "");
|
||||
const auto zones = payload.contains("zones") ? payload["zones"] : nlohmann::json::array();
|
||||
|
||||
std::error_code ec;
|
||||
@@ -131,10 +130,10 @@ std::optional<nlohmann::json> MapStore::create(const nlohmann::json& payload, st
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"INSERT INTO maps(id, name, description, width, height, resolution, "
|
||||
"INSERT INTO maps(id, name, description, site_id, created_by, width, height, resolution, "
|
||||
"origin_x, origin_y, origin_yaw, image_file, yaml_file, zones_json, "
|
||||
"created_at, updated_at) "
|
||||
"VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14)",
|
||||
"VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16)",
|
||||
-1,
|
||||
&stmt,
|
||||
nullptr) != SQLITE_OK)
|
||||
@@ -146,27 +145,32 @@ std::optional<nlohmann::json> MapStore::create(const nlohmann::json& payload, st
|
||||
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, name.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 3, description.c_str(), -1, SQLITE_TRANSIENT);
|
||||
if (payload.contains("width") && payload["width"].is_number())
|
||||
sqlite3_bind_double(stmt, 4, payload["width"].get<double>());
|
||||
else
|
||||
if (site_id.empty())
|
||||
sqlite3_bind_null(stmt, 4);
|
||||
if (payload.contains("height") && payload["height"].is_number())
|
||||
sqlite3_bind_double(stmt, 5, payload["height"].get<double>());
|
||||
else
|
||||
sqlite3_bind_null(stmt, 5);
|
||||
if (payload.contains("resolution") && payload["resolution"].is_number())
|
||||
sqlite3_bind_double(stmt, 6, payload["resolution"].get<double>());
|
||||
sqlite3_bind_text(stmt, 4, site_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 5, created_by.c_str(), -1, SQLITE_TRANSIENT);
|
||||
if (payload.contains("width") && payload["width"].is_number())
|
||||
sqlite3_bind_double(stmt, 6, payload["width"].get<double>());
|
||||
else
|
||||
sqlite3_bind_null(stmt, 6);
|
||||
sqlite3_bind_double(stmt, 7, payload.value("origin_x", 0.0));
|
||||
sqlite3_bind_double(stmt, 8, payload.value("origin_y", 0.0));
|
||||
sqlite3_bind_double(stmt, 9, payload.value("origin_yaw", 0.0));
|
||||
sqlite3_bind_null(stmt, 10);
|
||||
sqlite3_bind_null(stmt, 11);
|
||||
if (payload.contains("height") && payload["height"].is_number())
|
||||
sqlite3_bind_double(stmt, 7, payload["height"].get<double>());
|
||||
else
|
||||
sqlite3_bind_null(stmt, 7);
|
||||
if (payload.contains("resolution") && payload["resolution"].is_number())
|
||||
sqlite3_bind_double(stmt, 8, payload["resolution"].get<double>());
|
||||
else
|
||||
sqlite3_bind_null(stmt, 8);
|
||||
sqlite3_bind_double(stmt, 9, payload.value("origin_x", 0.0));
|
||||
sqlite3_bind_double(stmt, 10, payload.value("origin_y", 0.0));
|
||||
sqlite3_bind_double(stmt, 11, payload.value("origin_yaw", 0.0));
|
||||
sqlite3_bind_null(stmt, 12);
|
||||
sqlite3_bind_null(stmt, 13);
|
||||
const std::string zones_str = zones.dump();
|
||||
sqlite3_bind_text(stmt, 12, zones_str.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 13, now.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 14, now.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 14, zones_str.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 15, now.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 16, now.c_str(), -1, SQLITE_TRANSIENT);
|
||||
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE)
|
||||
{
|
||||
@@ -180,6 +184,8 @@ std::optional<nlohmann::json> MapStore::create(const nlohmann::json& payload, st
|
||||
created["id"] = id;
|
||||
created["name"] = name;
|
||||
created["description"] = description;
|
||||
created["site_id"] = site_id.empty() ? nullptr : nlohmann::json(site_id);
|
||||
created["created_by"] = created_by;
|
||||
if (payload.contains("width") && payload["width"].is_number())
|
||||
created["width"] = payload["width"];
|
||||
if (payload.contains("height") && payload["height"].is_number())
|
||||
@@ -207,7 +213,8 @@ bool MapStore::update(const std::string& id, const nlohmann::json& payload, std:
|
||||
}
|
||||
|
||||
nlohmann::json merged = *existing;
|
||||
for (const char* key : {"name", "description", "width", "height", "resolution", "origin_x", "origin_y", "origin_yaw"})
|
||||
for (const char* key :
|
||||
{"name", "description", "site_id", "created_by", "width", "height", "resolution", "origin_x", "origin_y", "origin_yaw"})
|
||||
{
|
||||
if (payload.contains(key))
|
||||
merged[key] = payload[key];
|
||||
@@ -221,8 +228,9 @@ bool MapStore::update(const std::string& id, const nlohmann::json& payload, std:
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"UPDATE maps SET name=?2, description=?3, width=?4, height=?5, resolution=?6, "
|
||||
"origin_x=?7, origin_y=?8, origin_yaw=?9, zones_json=?10, updated_at=?11 WHERE id=?1",
|
||||
"UPDATE maps SET name=?2, description=?3, site_id=?4, created_by=?5, width=?6, height=?7, "
|
||||
"resolution=?8, origin_x=?9, origin_y=?10, origin_yaw=?11, zones_json=?12, updated_at=?13 "
|
||||
"WHERE id=?1",
|
||||
-1,
|
||||
&stmt,
|
||||
nullptr) != SQLITE_OK)
|
||||
@@ -234,23 +242,29 @@ bool MapStore::update(const std::string& id, const nlohmann::json& payload, std:
|
||||
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, merged.value("name", "").c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 3, merged.value("description", "").c_str(), -1, SQLITE_TRANSIENT);
|
||||
if (merged["width"].is_number())
|
||||
sqlite3_bind_double(stmt, 4, merged["width"].get<double>());
|
||||
else
|
||||
const std::string site_id = merged.value("site_id", "");
|
||||
if (site_id.empty())
|
||||
sqlite3_bind_null(stmt, 4);
|
||||
if (merged["height"].is_number())
|
||||
sqlite3_bind_double(stmt, 5, merged["height"].get<double>());
|
||||
else
|
||||
sqlite3_bind_null(stmt, 5);
|
||||
if (merged["resolution"].is_number())
|
||||
sqlite3_bind_double(stmt, 6, merged["resolution"].get<double>());
|
||||
sqlite3_bind_text(stmt, 4, site_id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 5, merged.value("created_by", "").c_str(), -1, SQLITE_TRANSIENT);
|
||||
if (merged["width"].is_number())
|
||||
sqlite3_bind_double(stmt, 6, merged["width"].get<double>());
|
||||
else
|
||||
sqlite3_bind_null(stmt, 6);
|
||||
sqlite3_bind_double(stmt, 7, merged.value("origin_x", 0.0));
|
||||
sqlite3_bind_double(stmt, 8, merged.value("origin_y", 0.0));
|
||||
sqlite3_bind_double(stmt, 9, merged.value("origin_yaw", 0.0));
|
||||
sqlite3_bind_text(stmt, 10, zones_str.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 11, now.c_str(), -1, SQLITE_TRANSIENT);
|
||||
if (merged["height"].is_number())
|
||||
sqlite3_bind_double(stmt, 7, merged["height"].get<double>());
|
||||
else
|
||||
sqlite3_bind_null(stmt, 7);
|
||||
if (merged["resolution"].is_number())
|
||||
sqlite3_bind_double(stmt, 8, merged["resolution"].get<double>());
|
||||
else
|
||||
sqlite3_bind_null(stmt, 8);
|
||||
sqlite3_bind_double(stmt, 9, merged.value("origin_x", 0.0));
|
||||
sqlite3_bind_double(stmt, 10, merged.value("origin_y", 0.0));
|
||||
sqlite3_bind_double(stmt, 11, merged.value("origin_yaw", 0.0));
|
||||
sqlite3_bind_text(stmt, 12, zones_str.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 13, now.c_str(), -1, SQLITE_TRANSIENT);
|
||||
|
||||
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
|
||||
if (!ok)
|
||||
|
||||
242
src/storage/site_store.cpp
Normal file
242
src/storage/site_store.cpp
Normal file
@@ -0,0 +1,242 @@
|
||||
#include "storage/site_store.hpp"
|
||||
|
||||
#include "storage/database.hpp"
|
||||
#include "util/id_util.hpp"
|
||||
#include "util/string_util.hpp"
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
namespace lm {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kDefaultSiteId = "site_configuration";
|
||||
constexpr const char* kDefaultSiteName = "ConfigurationSite";
|
||||
|
||||
nlohmann::json rowToJson(sqlite3_stmt* stmt)
|
||||
{
|
||||
auto textOrNull = [&](int col) -> nlohmann::json {
|
||||
if (sqlite3_column_type(stmt, col) == SQLITE_NULL)
|
||||
return nullptr;
|
||||
return nlohmann::json(reinterpret_cast<const char*>(sqlite3_column_text(stmt, col)));
|
||||
};
|
||||
return {{"id", textOrNull(0)},
|
||||
{"name", textOrNull(1)},
|
||||
{"created_at", textOrNull(2)},
|
||||
{"updated_at", textOrNull(3)}};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SiteStore::SiteStore(Database& db) : db_(db) {}
|
||||
|
||||
std::string SiteStore::ensureDefaultSiteId()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(), "SELECT id FROM sites WHERE id = ?1", -1, &stmt, nullptr) != SQLITE_OK)
|
||||
return kDefaultSiteId;
|
||||
sqlite3_bind_text(stmt, 1, kDefaultSiteId, -1, SQLITE_STATIC);
|
||||
bool exists = sqlite3_step(stmt) == SQLITE_ROW;
|
||||
sqlite3_finalize(stmt);
|
||||
if (exists)
|
||||
return kDefaultSiteId;
|
||||
|
||||
const std::string now = IdUtil::nowIso8601();
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"INSERT INTO sites(id, name, created_at, updated_at) VALUES(?1,?2,?3,?4)",
|
||||
-1,
|
||||
&stmt,
|
||||
nullptr) != SQLITE_OK)
|
||||
return kDefaultSiteId;
|
||||
sqlite3_bind_text(stmt, 1, kDefaultSiteId, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 2, kDefaultSiteName, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 3, now.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 4, now.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
return kDefaultSiteId;
|
||||
}
|
||||
|
||||
nlohmann::json SiteStore::list() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
nlohmann::json sites = nlohmann::json::array();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"SELECT id, name, created_at, updated_at FROM sites ORDER BY name",
|
||||
-1,
|
||||
&stmt,
|
||||
nullptr) != SQLITE_OK)
|
||||
return sites;
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW)
|
||||
sites.push_back(rowToJson(stmt));
|
||||
sqlite3_finalize(stmt);
|
||||
return sites;
|
||||
}
|
||||
|
||||
std::optional<nlohmann::json> SiteStore::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, name, created_at, updated_at FROM sites 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> SiteStore::create(const nlohmann::json& payload, std::string& err)
|
||||
{
|
||||
if (!payload.is_object())
|
||||
{
|
||||
err = "payload must be an object";
|
||||
return std::nullopt;
|
||||
}
|
||||
const std::string name = StringUtil::trimCopy(payload.value("name", ""));
|
||||
if (name.empty())
|
||||
{
|
||||
err = "name is required";
|
||||
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 sites(id, name, created_at, updated_at) VALUES(?1,?2,?3,?4)",
|
||||
-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, name.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 3, now.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 4, 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}, {"name", name}, {"created_at", now}, {"updated_at", now}};
|
||||
}
|
||||
|
||||
bool SiteStore::update(const std::string& id, const nlohmann::json& payload, std::string& err)
|
||||
{
|
||||
auto existing = find(id);
|
||||
if (!existing)
|
||||
{
|
||||
err = "site not found";
|
||||
return false;
|
||||
}
|
||||
|
||||
nlohmann::json merged = *existing;
|
||||
if (payload.contains("name"))
|
||||
merged["name"] = payload["name"];
|
||||
|
||||
const std::string now = IdUtil::nowIso8601();
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"UPDATE sites SET name = ?2, updated_at = ?3 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, merged.value("name", "").c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 3, 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 SiteStore::remove(const std::string& id, std::string& err)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
|
||||
sqlite3_stmt* check = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(), "SELECT id FROM sites WHERE id = ?1", -1, &check, nullptr) != SQLITE_OK)
|
||||
{
|
||||
err = sqlite3_errmsg(db_.handle());
|
||||
return false;
|
||||
}
|
||||
sqlite3_bind_text(check, 1, id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
if (sqlite3_step(check) != SQLITE_ROW)
|
||||
{
|
||||
sqlite3_finalize(check);
|
||||
err = "site not found";
|
||||
return false;
|
||||
}
|
||||
sqlite3_finalize(check);
|
||||
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"SELECT COUNT(*) FROM maps WHERE site_id = ?1",
|
||||
-1,
|
||||
&check,
|
||||
nullptr) != SQLITE_OK)
|
||||
{
|
||||
err = sqlite3_errmsg(db_.handle());
|
||||
return false;
|
||||
}
|
||||
sqlite3_bind_text(check, 1, id.c_str(), -1, SQLITE_TRANSIENT);
|
||||
int map_count = 0;
|
||||
if (sqlite3_step(check) == SQLITE_ROW)
|
||||
map_count = sqlite3_column_int(check, 0);
|
||||
sqlite3_finalize(check);
|
||||
if (map_count > 0)
|
||||
{
|
||||
err = "site has maps and cannot be deleted";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sqlite3_prepare_v2(db_.handle(), "SELECT COUNT(*) FROM sites", -1, &check, nullptr) != SQLITE_OK)
|
||||
{
|
||||
err = sqlite3_errmsg(db_.handle());
|
||||
return false;
|
||||
}
|
||||
int site_count = 0;
|
||||
if (sqlite3_step(check) == SQLITE_ROW)
|
||||
site_count = sqlite3_column_int(check, 0);
|
||||
sqlite3_finalize(check);
|
||||
if (site_count <= 1)
|
||||
{
|
||||
err = "cannot delete the last site";
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(), "DELETE FROM sites 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
|
||||
30
src/storage/site_store.hpp
Normal file
30
src/storage/site_store.hpp
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace lm {
|
||||
|
||||
class Database;
|
||||
|
||||
class SiteStore
|
||||
{
|
||||
public:
|
||||
explicit SiteStore(Database& db);
|
||||
|
||||
nlohmann::json list() const;
|
||||
std::optional<nlohmann::json> find(const std::string& 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);
|
||||
std::string ensureDefaultSiteId();
|
||||
|
||||
private:
|
||||
Database& db_;
|
||||
mutable std::mutex mu_;
|
||||
};
|
||||
|
||||
} // namespace lm
|
||||
Reference in New Issue
Block a user