|
@@ -0,0 +1,293 @@
|
|
|
|
|
+#include "smartbotic/database/collection_meta.hpp"
|
|
|
|
|
+
|
|
|
|
|
+#include <spdlog/spdlog.h>
|
|
|
|
|
+
|
|
|
|
|
+#include <filesystem>
|
|
|
|
|
+
|
|
|
|
|
+namespace smartbotic::database {
|
|
|
|
|
+
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+// TTLConfig Implementation
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+
|
|
|
|
|
+auto TTLConfig::ToJson() const -> nlohmann::json {
|
|
|
|
|
+ return nlohmann::json{
|
|
|
|
|
+ {"enabled", enabled},
|
|
|
|
|
+ {"ttl_seconds", ttl_seconds},
|
|
|
|
|
+ {"field_name", field_name},
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto TTLConfig::FromJson(const nlohmann::json& json) -> TTLConfig {
|
|
|
|
|
+ TTLConfig config;
|
|
|
|
|
+
|
|
|
|
|
+ if (json.contains("enabled") && json["enabled"].is_boolean()) {
|
|
|
|
|
+ config.enabled = json["enabled"].get<bool>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (json.contains("ttl_seconds") && json["ttl_seconds"].is_number_integer()) {
|
|
|
|
|
+ config.ttl_seconds = json["ttl_seconds"].get<int64_t>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (json.contains("field_name") && json["field_name"].is_string()) {
|
|
|
|
|
+ config.field_name = json["field_name"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return config;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+// EncryptionConfig Implementation
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+
|
|
|
|
|
+auto EncryptionConfig::ToJson() const -> nlohmann::json {
|
|
|
|
|
+ nlohmann::json fields_array = nlohmann::json::array();
|
|
|
|
|
+ for (const auto& field : encrypted_fields) {
|
|
|
|
|
+ fields_array.push_back(field);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return nlohmann::json{
|
|
|
|
|
+ {"encrypted_fields", fields_array},
|
|
|
|
|
+ {"key_id", key_id},
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto EncryptionConfig::FromJson(const nlohmann::json& json) -> EncryptionConfig {
|
|
|
|
|
+ EncryptionConfig config;
|
|
|
|
|
+
|
|
|
|
|
+ if (json.contains("encrypted_fields") && json["encrypted_fields"].is_array()) {
|
|
|
|
|
+ for (const auto& field : json["encrypted_fields"]) {
|
|
|
|
|
+ if (field.is_string()) {
|
|
|
|
|
+ config.encrypted_fields.insert(field.get<std::string>());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (json.contains("key_id") && json["key_id"].is_string()) {
|
|
|
|
|
+ config.key_id = json["key_id"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return config;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto EncryptionConfig::IsFieldEncrypted(std::string_view field_path) const -> bool {
|
|
|
|
|
+ return encrypted_fields.contains(std::string(field_path));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void EncryptionConfig::AddEncryptedField(std::string field_path) {
|
|
|
|
|
+ encrypted_fields.insert(std::move(field_path));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void EncryptionConfig::RemoveEncryptedField(std::string_view field_path) {
|
|
|
|
|
+ encrypted_fields.erase(std::string(field_path));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+// CollectionMeta Implementation
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+
|
|
|
|
|
+CollectionMeta::CollectionMeta(std::string collection_name)
|
|
|
|
|
+ : name(std::move(collection_name)),
|
|
|
|
|
+ is_system(name.starts_with("_")),
|
|
|
|
|
+ created_at(Timestamp::Now()),
|
|
|
|
|
+ updated_at(created_at) {}
|
|
|
|
|
+
|
|
|
|
|
+CollectionMeta::CollectionMeta(std::string collection_name, nlohmann::json json_schema)
|
|
|
|
|
+ : name(std::move(collection_name)),
|
|
|
|
|
+ schema(std::move(json_schema)),
|
|
|
|
|
+ is_system(name.starts_with("_")),
|
|
|
|
|
+ created_at(Timestamp::Now()),
|
|
|
|
|
+ updated_at(created_at) {}
|
|
|
|
|
+
|
|
|
|
|
+auto CollectionMeta::ToJson() const -> nlohmann::json {
|
|
|
|
|
+ return nlohmann::json{
|
|
|
|
|
+ {"name", name},
|
|
|
|
|
+ {"schema", schema.is_null() ? nlohmann::json::object() : schema},
|
|
|
|
|
+ {"is_system", is_system},
|
|
|
|
|
+ {"encryption", encryption.ToJson()},
|
|
|
|
|
+ {"ttl_config", ttl_config.ToJson()},
|
|
|
|
|
+ {"created_at", created_at.ToJson()},
|
|
|
|
|
+ {"updated_at", updated_at.ToJson()},
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto CollectionMeta::FromJson(const nlohmann::json& json) -> CollectionMeta {
|
|
|
|
|
+ CollectionMeta meta;
|
|
|
|
|
+
|
|
|
|
|
+ if (json.contains("name") && json["name"].is_string()) {
|
|
|
|
|
+ meta.name = json["name"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (json.contains("schema") && json["schema"].is_object()) {
|
|
|
|
|
+ meta.schema = json["schema"];
|
|
|
|
|
+ // If schema is empty object, treat as null (no schema)
|
|
|
|
|
+ if (meta.schema.empty()) {
|
|
|
|
|
+ meta.schema = nullptr;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (json.contains("is_system") && json["is_system"].is_boolean()) {
|
|
|
|
|
+ meta.is_system = json["is_system"].get<bool>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (json.contains("encryption") && json["encryption"].is_object()) {
|
|
|
|
|
+ meta.encryption = EncryptionConfig::FromJson(json["encryption"]);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (json.contains("ttl_config") && json["ttl_config"].is_object()) {
|
|
|
|
|
+ meta.ttl_config = TTLConfig::FromJson(json["ttl_config"]);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (json.contains("created_at") && json["created_at"].is_object()) {
|
|
|
|
|
+ meta.created_at = Timestamp::FromJson(json["created_at"]);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (json.contains("updated_at") && json["updated_at"].is_object()) {
|
|
|
|
|
+ meta.updated_at = Timestamp::FromJson(json["updated_at"]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return meta;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void CollectionMeta::Touch() {
|
|
|
|
|
+ updated_at = Timestamp::Now();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto CollectionMeta::IsSystemCollection() const -> bool {
|
|
|
|
|
+ return is_system || name.starts_with("_");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+// CollectionMetaStore Implementation
|
|
|
|
|
+// ============================================================================
|
|
|
|
|
+
|
|
|
|
|
+CollectionMetaStore::CollectionMetaStore(std::string file_path) : file_path_(std::move(file_path)) {}
|
|
|
|
|
+
|
|
|
|
|
+auto CollectionMetaStore::Exists() const -> bool {
|
|
|
|
|
+ return std::filesystem::exists(file_path_);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto CollectionMetaStore::Load() -> std::unordered_map<std::string, CollectionMeta> {
|
|
|
|
|
+ std::unordered_map<std::string, CollectionMeta> result;
|
|
|
|
|
+
|
|
|
|
|
+ if (!Exists()) {
|
|
|
|
|
+ spdlog::debug("Collection metadata file '{}' does not exist, returning empty map", file_path_);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::ifstream file(file_path_, std::ios::binary);
|
|
|
|
|
+ if (!file.is_open()) {
|
|
|
|
|
+ spdlog::error("Failed to open collection metadata file '{}'", file_path_);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Read and validate header
|
|
|
|
|
+ CollectionMetaFileHeader header{};
|
|
|
|
|
+ file.read(reinterpret_cast<char*>(&header), sizeof(header));
|
|
|
|
|
+
|
|
|
|
|
+ if (file.gcount() != sizeof(header)) {
|
|
|
|
|
+ spdlog::error("Failed to read collection metadata header from '{}'", file_path_);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (header.magic != kCollectionMetaMagic) {
|
|
|
|
|
+ spdlog::error("Invalid magic number in collection metadata file '{}': expected 0x{:08X}, got 0x{:08X}",
|
|
|
|
|
+ file_path_, kCollectionMetaMagic, header.magic);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (header.version != kCollectionMetaVersion) {
|
|
|
|
|
+ spdlog::error("Unsupported collection metadata version in '{}': expected {}, got {}", file_path_,
|
|
|
|
|
+ kCollectionMetaVersion, header.version);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Read each collection's metadata
|
|
|
|
|
+ for (uint64_t i = 0; i < header.collection_count; ++i) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ nlohmann::json json = ReadJson(file);
|
|
|
|
|
+ CollectionMeta meta = CollectionMeta::FromJson(json);
|
|
|
|
|
+ result.emplace(meta.name, std::move(meta));
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Failed to read collection metadata entry {}: {}", i, e.what());
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::info("Loaded {} collection metadata entries from '{}'", result.size(), file_path_);
|
|
|
|
|
+ return result;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto CollectionMetaStore::Save(const std::unordered_map<std::string, CollectionMeta>& metadata) -> bool {
|
|
|
|
|
+ // Create parent directories if needed
|
|
|
|
|
+ std::filesystem::path path(file_path_);
|
|
|
|
|
+ if (path.has_parent_path()) {
|
|
|
|
|
+ std::error_code ec;
|
|
|
|
|
+ std::filesystem::create_directories(path.parent_path(), ec);
|
|
|
|
|
+ if (ec) {
|
|
|
|
|
+ spdlog::error("Failed to create directory '{}': {}", path.parent_path().string(), ec.message());
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::ofstream file(file_path_, std::ios::binary | std::ios::trunc);
|
|
|
|
|
+ if (!file.is_open()) {
|
|
|
|
|
+ spdlog::error("Failed to open collection metadata file '{}' for writing", file_path_);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Write header
|
|
|
|
|
+ CollectionMetaFileHeader header{};
|
|
|
|
|
+ header.magic = kCollectionMetaMagic;
|
|
|
|
|
+ header.version = kCollectionMetaVersion;
|
|
|
|
|
+ header.collection_count = metadata.size();
|
|
|
|
|
+ header.reserved = 0;
|
|
|
|
|
+
|
|
|
|
|
+ file.write(reinterpret_cast<const char*>(&header), sizeof(header));
|
|
|
|
|
+
|
|
|
|
|
+ // Write each collection's metadata as JSON
|
|
|
|
|
+ for (const auto& [name, meta] : metadata) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ WriteJson(file, meta.ToJson());
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Failed to write collection metadata for '{}': {}", name, e.what());
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ file.flush();
|
|
|
|
|
+ if (!file.good()) {
|
|
|
|
|
+ spdlog::error("Failed to write collection metadata to '{}'", file_path_);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::info("Saved {} collection metadata entries to '{}'", metadata.size(), file_path_);
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void CollectionMetaStore::WriteString(std::ofstream& stream, const std::string& str) {
|
|
|
|
|
+ auto length = static_cast<uint32_t>(str.size());
|
|
|
|
|
+ stream.write(reinterpret_cast<const char*>(&length), sizeof(length));
|
|
|
|
|
+ if (!str.empty()) {
|
|
|
|
|
+ stream.write(str.data(), static_cast<std::streamsize>(str.size()));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto CollectionMetaStore::ReadString(std::ifstream& stream) -> std::string {
|
|
|
|
|
+ uint32_t length = 0;
|
|
|
|
|
+ stream.read(reinterpret_cast<char*>(&length), sizeof(length));
|
|
|
|
|
+
|
|
|
|
|
+ if (length == 0) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::string result(length, '\0');
|
|
|
|
|
+ stream.read(result.data(), length);
|
|
|
|
|
+ return result;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void CollectionMetaStore::WriteJson(std::ofstream& stream, const nlohmann::json& json) {
|
|
|
|
|
+ std::string serialized = json.dump();
|
|
|
|
|
+ WriteString(stream, serialized);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto CollectionMetaStore::ReadJson(std::ifstream& stream) -> nlohmann::json {
|
|
|
|
|
+ std::string serialized = ReadString(stream);
|
|
|
|
|
+ if (serialized.empty()) {
|
|
|
|
|
+ return nlohmann::json::object();
|
|
|
|
|
+ }
|
|
|
|
|
+ return nlohmann::json::parse(serialized);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+} // namespace smartbotic::database
|