This commit is contained in:
@@ -108,6 +108,20 @@ CREATE TABLE IF NOT EXISTS transitions (
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS io_modules (
|
||||
id TEXT PRIMARY KEY,
|
||||
site_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
module_type TEXT NOT NULL,
|
||||
ip_address TEXT NOT NULL,
|
||||
created_by TEXT NOT NULL DEFAULT '',
|
||||
created_by_group TEXT NOT NULL DEFAULT '',
|
||||
connected INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dashboards (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
@@ -395,6 +409,28 @@ bool Database::applySchemaMigrations(std::string& err)
|
||||
setMeta("schema_version", "6");
|
||||
}
|
||||
|
||||
ver = getMeta("schema_version").value_or("1");
|
||||
if (ver == "6")
|
||||
{
|
||||
if (!execSql(db_,
|
||||
"CREATE TABLE IF NOT EXISTS io_modules ("
|
||||
"id TEXT PRIMARY KEY, "
|
||||
"site_id TEXT NOT NULL, "
|
||||
"name TEXT NOT NULL, "
|
||||
"module_type TEXT NOT NULL, "
|
||||
"ip_address TEXT NOT NULL, "
|
||||
"created_by TEXT NOT NULL DEFAULT '', "
|
||||
"created_by_group TEXT NOT NULL DEFAULT '', "
|
||||
"connected INTEGER NOT NULL DEFAULT 0, "
|
||||
"created_at TEXT NOT NULL, "
|
||||
"updated_at TEXT NOT NULL, "
|
||||
"FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE"
|
||||
")",
|
||||
err))
|
||||
return false;
|
||||
setMeta("schema_version", "7");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
324
src/storage/io_module_store.cpp
Normal file
324
src/storage/io_module_store.cpp
Normal file
@@ -0,0 +1,324 @@
|
||||
#include "storage/io_module_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)},
|
||||
{"name", text(2)},
|
||||
{"module_type", text(3)},
|
||||
{"ip_address", text(4)},
|
||||
{"created_by", text(5)},
|
||||
{"created_by_group", text(6)},
|
||||
{"connected", sqlite3_column_int(stmt, 7) != 0},
|
||||
{"created_at", text(8)},
|
||||
{"updated_at", text(9)}};
|
||||
}
|
||||
|
||||
constexpr const char* kSelect =
|
||||
"SELECT id, site_id, name, module_type, ip_address, created_by, created_by_group, connected, created_at, updated_at "
|
||||
"FROM io_modules";
|
||||
|
||||
bool validModuleType(const std::string& t)
|
||||
{
|
||||
return t == "bluetooth" || t == "wise";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
IoModuleStore::IoModuleStore(Database& db) : db_(db) {}
|
||||
|
||||
bool IoModuleStore::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 io_modules WHERE site_id = ?1 AND lower(name) = lower(?2) LIMIT 1"
|
||||
: "SELECT id FROM io_modules 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 IoModuleStore::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 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> IoModuleStore::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> IoModuleStore::findByName(const std::string& name) const
|
||||
{
|
||||
const std::string needle = StringUtil::toLower(StringUtil::trimCopy(name));
|
||||
if (needle.empty())
|
||||
return std::nullopt;
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"SELECT id, site_id, name, module_type, ip_address, created_by, created_by_group, connected, "
|
||||
"created_at, updated_at FROM io_modules WHERE lower(name) = lower(?1) LIMIT 1",
|
||||
-1,
|
||||
&stmt,
|
||||
nullptr) != SQLITE_OK)
|
||||
return std::nullopt;
|
||||
sqlite3_bind_text(stmt, 1, name.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> IoModuleStore::resolveRef(const std::string& ref) const
|
||||
{
|
||||
if (auto by_id = find(ref))
|
||||
return by_id;
|
||||
return findByName(ref);
|
||||
}
|
||||
|
||||
std::optional<nlohmann::json> IoModuleStore::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 name = StringUtil::trimCopy(payload.value("name", ""));
|
||||
std::string module_type = StringUtil::toLower(StringUtil::trimCopy(payload.value("module_type", "")));
|
||||
const std::string ip_address = StringUtil::trimCopy(payload.value("ip_address", ""));
|
||||
const std::string created_by = StringUtil::trimCopy(payload.value("created_by", ""));
|
||||
const std::string created_by_group = StringUtil::trimCopy(payload.value("created_by_group", ""));
|
||||
|
||||
if (site_id.empty() || name.empty() || module_type.empty() || ip_address.empty())
|
||||
{
|
||||
err = "missing required fields";
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!validModuleType(module_type))
|
||||
{
|
||||
err = "module_type must be bluetooth or wise";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
if (findNameConflictUnlocked(site_id, name, ""))
|
||||
{
|
||||
err = "io module name already exists for this site";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const std::string id = payload.value("id", IdUtil::newId());
|
||||
const std::string now = IdUtil::nowIso8601();
|
||||
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"INSERT INTO io_modules(id, site_id, name, module_type, ip_address, created_by, "
|
||||
"created_by_group, connected, created_at, updated_at) "
|
||||
"VALUES(?1,?2,?3,?4,?5,?6,?7,0,?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, name.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 4, module_type.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 5, ip_address.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);
|
||||
|
||||
return nlohmann::json{{"id", id},
|
||||
{"site_id", site_id},
|
||||
{"name", name},
|
||||
{"module_type", module_type},
|
||||
{"ip_address", ip_address},
|
||||
{"created_by", created_by},
|
||||
{"created_by_group", created_by_group},
|
||||
{"connected", false},
|
||||
{"created_at", now},
|
||||
{"updated_at", now}};
|
||||
}
|
||||
|
||||
bool IoModuleStore::update(const std::string& id, const nlohmann::json& payload, std::string& err)
|
||||
{
|
||||
auto existing = find(id);
|
||||
if (!existing)
|
||||
{
|
||||
err = "io module not found";
|
||||
return false;
|
||||
}
|
||||
|
||||
nlohmann::json merged = *existing;
|
||||
for (const char* key : {"site_id", "name", "module_type", "ip_address"})
|
||||
{
|
||||
if (payload.contains(key))
|
||||
merged[key] = payload[key];
|
||||
}
|
||||
|
||||
const std::string site_id = StringUtil::trimCopy(merged.value("site_id", ""));
|
||||
const std::string name = StringUtil::trimCopy(merged.value("name", ""));
|
||||
std::string module_type = StringUtil::toLower(StringUtil::trimCopy(merged.value("module_type", "")));
|
||||
const std::string ip_address = StringUtil::trimCopy(merged.value("ip_address", ""));
|
||||
|
||||
if (site_id.empty() || name.empty() || module_type.empty() || ip_address.empty())
|
||||
{
|
||||
err = "missing required fields";
|
||||
return false;
|
||||
}
|
||||
if (!validModuleType(module_type))
|
||||
{
|
||||
err = "module_type must be bluetooth or wise";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
if (findNameConflictUnlocked(site_id, name, id))
|
||||
{
|
||||
err = "io module name already exists for this site";
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string now = IdUtil::nowIso8601();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"UPDATE io_modules SET site_id=?2, name=?3, module_type=?4, ip_address=?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, name.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 4, module_type.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 5, ip_address.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 IoModuleStore::remove(const std::string& id, std::string& err)
|
||||
{
|
||||
if (!find(id))
|
||||
{
|
||||
err = "io module not found";
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(), "DELETE FROM io_modules 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;
|
||||
}
|
||||
|
||||
bool IoModuleStore::setConnected(const std::string& id, bool connected, std::string& err)
|
||||
{
|
||||
if (!find(id))
|
||||
{
|
||||
err = "io module not found";
|
||||
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 io_modules SET connected=?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_int(stmt, 2, connected ? 1 : 0);
|
||||
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;
|
||||
}
|
||||
|
||||
} // namespace lm
|
||||
36
src/storage/io_module_store.hpp
Normal file
36
src/storage/io_module_store.hpp
Normal file
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace lm {
|
||||
|
||||
class Database;
|
||||
|
||||
class IoModuleStore
|
||||
{
|
||||
public:
|
||||
explicit IoModuleStore(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> findByName(const std::string& name) const;
|
||||
std::optional<nlohmann::json> resolveRef(const std::string& ref) 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);
|
||||
bool setConnected(const std::string& id, bool connected, std::string& err);
|
||||
|
||||
private:
|
||||
Database& db_;
|
||||
mutable std::mutex mu_;
|
||||
|
||||
bool findNameConflictUnlocked(const std::string& site_id,
|
||||
const std::string& name,
|
||||
const std::string& except_id) const;
|
||||
};
|
||||
|
||||
} // namespace lm
|
||||
Reference in New Issue
Block a user