thêm phần paths
Some checks failed
Test / test (push) Has been cancelled

This commit is contained in:
2026-06-23 17:51:54 +07:00
parent 50a2587cef
commit 2ce8a23ce9
25 changed files with 1411 additions and 31 deletions

View File

@@ -122,6 +122,21 @@ CREATE TABLE IF NOT EXISTS io_modules (
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS paths (
id TEXT PRIMARY KEY,
site_id TEXT NOT NULL,
map_id TEXT NOT NULL,
from_position_id TEXT NOT NULL,
to_position_id TEXT NOT NULL,
auto_created INTEGER NOT NULL DEFAULT 1,
points_json TEXT NOT NULL DEFAULT '[]',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(map_id, from_position_id, to_position_id),
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE,
FOREIGN KEY (map_id) REFERENCES maps(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS dashboards (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
@@ -431,6 +446,29 @@ bool Database::applySchemaMigrations(std::string& err)
setMeta("schema_version", "7");
}
ver = getMeta("schema_version").value_or("1");
if (ver == "7")
{
if (!execSql(db_,
"CREATE TABLE IF NOT EXISTS paths ("
"id TEXT PRIMARY KEY, "
"site_id TEXT NOT NULL, "
"map_id TEXT NOT NULL, "
"from_position_id TEXT NOT NULL, "
"to_position_id TEXT NOT NULL, "
"auto_created INTEGER NOT NULL DEFAULT 1, "
"points_json TEXT NOT NULL DEFAULT '[]', "
"created_at TEXT NOT NULL, "
"updated_at TEXT NOT NULL, "
"UNIQUE(map_id, from_position_id, to_position_id), "
"FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE, "
"FOREIGN KEY (map_id) REFERENCES maps(id) ON DELETE CASCADE"
")",
err))
return false;
setMeta("schema_version", "8");
}
return true;
}

256
src/storage/path_store.cpp Normal file
View File

@@ -0,0 +1,256 @@
#include "storage/path_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) : "";
};
nlohmann::json points = nlohmann::json::array();
try
{
const std::string raw = text(6);
if (!raw.empty())
points = nlohmann::json::parse(raw);
}
catch (...)
{
points = nlohmann::json::array();
}
return {{"id", text(0)},
{"site_id", text(1)},
{"map_id", text(2)},
{"from_position_id", text(3)},
{"to_position_id", text(4)},
{"auto_created", sqlite3_column_int(stmt, 5) != 0},
{"points", points.is_array() ? points : nlohmann::json::array()},
{"created_at", text(7)},
{"updated_at", text(8)}};
}
constexpr const char* kSelect =
"SELECT id, site_id, map_id, from_position_id, to_position_id, auto_created, points_json, created_at, updated_at "
"FROM paths";
} // namespace
PathStore::PathStore(Database& db) : db_(db) {}
nlohmann::json PathStore::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 map_id, from_position_id, to_position_id";
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> PathStore::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> PathStore::findBetween(const std::string& map_id,
const std::string& from_position_id,
const std::string& to_position_id) const
{
if (map_id.empty() || from_position_id.empty() || to_position_id.empty())
return std::nullopt;
std::lock_guard<std::mutex> lock(mu_);
const std::string sql = std::string(kSelect) +
" WHERE map_id = ?1 AND from_position_id = ?2 AND to_position_id = ?3 LIMIT 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, map_id.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, from_position_id.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, to_position_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> PathStore::upsert(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 from_id = StringUtil::trimCopy(payload.value("from_position_id", ""));
const std::string to_id = StringUtil::trimCopy(payload.value("to_position_id", ""));
if (site_id.empty() || map_id.empty() || from_id.empty() || to_id.empty())
{
err = "missing required fields";
return std::nullopt;
}
if (from_id == to_id)
{
err = "from and to positions must differ";
return std::nullopt;
}
const auto& points = payload.contains("points") && payload["points"].is_array() ? payload["points"]
: nlohmann::json::array();
const std::string points_json = points.dump();
const bool auto_created = payload.value("auto_created", true);
const std::string now = IdUtil::nowIso8601();
std::string id = payload.value("id", IdUtil::newId());
{
std::lock_guard<std::mutex> lock(mu_);
sqlite3_stmt* lookup = nullptr;
if (sqlite3_prepare_v2(db_.handle(),
"SELECT id FROM paths WHERE map_id = ?1 AND from_position_id = ?2 AND to_position_id = ?3",
-1,
&lookup,
nullptr) == SQLITE_OK)
{
sqlite3_bind_text(lookup, 1, map_id.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(lookup, 2, from_id.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(lookup, 3, to_id.c_str(), -1, SQLITE_TRANSIENT);
if (sqlite3_step(lookup) == SQLITE_ROW)
{
const char* existing = reinterpret_cast<const char*>(sqlite3_column_text(lookup, 0));
if (existing)
id = existing;
}
sqlite3_finalize(lookup);
}
}
std::lock_guard<std::mutex> lock(mu_);
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_.handle(),
"INSERT INTO paths(id, site_id, map_id, from_position_id, to_position_id, auto_created, "
"points_json, created_at, updated_at) "
"VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9) "
"ON CONFLICT(map_id, from_position_id, to_position_id) DO UPDATE SET "
"points_json=excluded.points_json, auto_created=excluded.auto_created, updated_at=excluded.updated_at",
-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, from_id.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 5, to_id.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 6, auto_created ? 1 : 0);
sqlite3_bind_text(stmt, 7, points_json.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);
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
if (!ok)
{
err = sqlite3_errmsg(db_.handle());
return std::nullopt;
}
sqlite3_stmt* get_stmt = nullptr;
if (sqlite3_prepare_v2(db_.handle(), (std::string(kSelect) + " WHERE id = ?1").c_str(), -1, &get_stmt, nullptr) !=
SQLITE_OK)
{
err = sqlite3_errmsg(db_.handle());
return std::nullopt;
}
sqlite3_bind_text(get_stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
std::optional<nlohmann::json> out;
if (sqlite3_step(get_stmt) == SQLITE_ROW)
out = rowToJson(get_stmt);
sqlite3_finalize(get_stmt);
return out;
}
bool PathStore::remove(const std::string& id, std::string& err)
{
std::lock_guard<std::mutex> lock(mu_);
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_.handle(), "DELETE FROM paths 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_step(stmt);
const bool ok = sqlite3_changes(db_.handle()) > 0;
sqlite3_finalize(stmt);
if (!ok)
err = "path not found";
return ok;
}
int PathStore::removeForMap(const std::string& map_id)
{
std::lock_guard<std::mutex> lock(mu_);
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_.handle(), "DELETE FROM paths WHERE map_id = ?1", -1, &stmt, nullptr) != SQLITE_OK)
return 0;
sqlite3_bind_text(stmt, 1, map_id.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_step(stmt);
const int n = sqlite3_changes(db_.handle());
sqlite3_finalize(stmt);
return n;
}
int PathStore::removeForPosition(const std::string& map_id, const std::string& position_id)
{
if (map_id.empty() || position_id.empty())
return 0;
std::lock_guard<std::mutex> lock(mu_);
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_.handle(),
"DELETE FROM paths WHERE map_id = ?1 AND (from_position_id = ?2 OR to_position_id = ?2)",
-1,
&stmt,
nullptr) != SQLITE_OK)
return 0;
sqlite3_bind_text(stmt, 1, map_id.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, position_id.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_step(stmt);
const int n = sqlite3_changes(db_.handle());
sqlite3_finalize(stmt);
return n;
}
} // namespace lm

View File

@@ -0,0 +1,33 @@
#pragma once
#include <nlohmann/json.hpp>
#include <mutex>
#include <optional>
#include <string>
namespace lm {
class Database;
class PathStore
{
public:
explicit PathStore(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& map_id,
const std::string& from_position_id,
const std::string& to_position_id) const;
std::optional<nlohmann::json> upsert(const nlohmann::json& payload, std::string& err);
bool remove(const std::string& id, std::string& err);
int removeForMap(const std::string& map_id);
int removeForPosition(const std::string& map_id, const std::string& position_id);
private:
Database& db_;
mutable std::mutex mu_;
};
} // namespace lm