|
@@ -0,0 +1,945 @@
|
|
|
|
|
+#include "smartbotic/webserver/archive_service.hpp"
|
|
|
|
|
+
|
|
|
|
|
+#include <chrono>
|
|
|
|
|
+#include <iomanip>
|
|
|
|
|
+#include <sstream>
|
|
|
|
|
+
|
|
|
|
|
+#include <spdlog/spdlog.h>
|
|
|
|
|
+
|
|
|
|
|
+#include "smartbotic/webserver/collection_service.hpp"
|
|
|
|
|
+#include "smartbotic/webserver/document_service.hpp"
|
|
|
|
|
+#include "smartbotic/webserver/page_service.hpp"
|
|
|
|
|
+#include "smartbotic/webserver/view_service.hpp"
|
|
|
|
|
+
|
|
|
|
|
+namespace smartbotic::webserver {
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveStatusToString(ArchiveStatus status) -> std::string {
|
|
|
|
|
+ switch (status) {
|
|
|
|
|
+ case ArchiveStatus::Active: return "active";
|
|
|
|
|
+ case ArchiveStatus::Expired: return "expired";
|
|
|
|
|
+ case ArchiveStatus::Restored: return "restored";
|
|
|
|
|
+ case ArchiveStatus::Exported: return "exported";
|
|
|
|
|
+ }
|
|
|
|
|
+ return "active";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto StringToArchiveStatus(const std::string& str) -> ArchiveStatus {
|
|
|
|
|
+ if (str == "expired") return ArchiveStatus::Expired;
|
|
|
|
|
+ if (str == "restored") return ArchiveStatus::Restored;
|
|
|
|
|
+ if (str == "exported") return ArchiveStatus::Exported;
|
|
|
|
|
+ return ArchiveStatus::Active;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// EntityTypeToString is now defined in event_manager.cpp
|
|
|
|
|
+
|
|
|
|
|
+auto StringToEntityType(const std::string& str) -> EntityType {
|
|
|
|
|
+ if (str == "page") return EntityType::Page;
|
|
|
|
|
+ if (str == "view") return EntityType::View;
|
|
|
|
|
+ if (str == "collection") return EntityType::Collection;
|
|
|
|
|
+ if (str == "document") return EntityType::Document;
|
|
|
|
|
+ if (str == "session") return EntityType::Session;
|
|
|
|
|
+ if (str == "user") return EntityType::User;
|
|
|
|
|
+ if (str == "group") return EntityType::Group;
|
|
|
|
|
+ if (str == "membership") return EntityType::Membership;
|
|
|
|
|
+ return EntityType::Workspace;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+namespace {
|
|
|
|
|
+
|
|
|
|
|
+auto SetStringValue(::smartbotic::database::MapValue* map, const std::string& key, const std::string& value) {
|
|
|
|
|
+ auto* field = &(*map->mutable_fields())[key];
|
|
|
|
|
+ field->set_string_value(value);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto SetIntValue(::smartbotic::database::MapValue* map, const std::string& key, int64_t value) {
|
|
|
|
|
+ auto* field = &(*map->mutable_fields())[key];
|
|
|
|
|
+ field->set_int_value(value);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto GetStringValue(const ::smartbotic::database::MapValue& map, const std::string& key) -> std::string {
|
|
|
|
|
+ auto it = map.fields().find(key);
|
|
|
|
|
+ if (it != map.fields().end() && it->second.has_string_value()) {
|
|
|
|
|
+ return it->second.string_value();
|
|
|
|
|
+ }
|
|
|
|
|
+ return "";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto GetIntValue(const ::smartbotic::database::MapValue& map, const std::string& key) -> int64_t {
|
|
|
|
|
+ auto it = map.fields().find(key);
|
|
|
|
|
+ if (it != map.fields().end() && it->second.has_int_value()) {
|
|
|
|
|
+ return it->second.int_value();
|
|
|
|
|
+ }
|
|
|
|
|
+ return 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+} // namespace
|
|
|
|
|
+
|
|
|
|
|
+ArchiveService::ArchiveService(DatabaseClient& db_client) : db_client_(db_client) {}
|
|
|
|
|
+
|
|
|
|
|
+ArchiveService::~ArchiveService() = default;
|
|
|
|
|
+
|
|
|
|
|
+ArchiveService::ArchiveService(ArchiveService&& other) noexcept
|
|
|
|
|
+ : db_client_(other.db_client_), initialized_(other.initialized_) {}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::operator=(ArchiveService&& /*other*/) noexcept -> ArchiveService& {
|
|
|
|
|
+ return *this;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::Initialize() -> bool {
|
|
|
|
|
+ if (initialized_) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto* collection_service = db_client_.GetCollectionService();
|
|
|
|
|
+ if (collection_service == nullptr) {
|
|
|
|
|
+ spdlog::error("Collection service not available");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if _archives collection exists
|
|
|
|
|
+ ::smartbotic::database::GetCollectionMetadataRequest get_req;
|
|
|
|
|
+ get_req.set_name(kArchivesCollection);
|
|
|
|
|
+
|
|
|
|
|
+ grpc::ClientContext get_ctx;
|
|
|
|
|
+ ::smartbotic::database::CollectionMetadata metadata;
|
|
|
|
|
+ auto status = collection_service->GetCollectionMetadata(&get_ctx, get_req, &metadata);
|
|
|
|
|
+
|
|
|
|
|
+ if (status.ok()) {
|
|
|
|
|
+ spdlog::info("System collection {} already exists", kArchivesCollection);
|
|
|
|
|
+ initialized_ = true;
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Collection doesn't exist, create it
|
|
|
|
|
+ ::smartbotic::database::CreateCollectionRequest create_req;
|
|
|
|
|
+ create_req.set_name(kArchivesCollection);
|
|
|
|
|
+
|
|
|
|
|
+ // Add indexes for common queries
|
|
|
|
|
+ auto* ws_index = create_req.add_indexes();
|
|
|
|
|
+ ws_index->set_collection(kArchivesCollection);
|
|
|
|
|
+ ws_index->set_index_name("workspace_id_idx");
|
|
|
|
|
+ ws_index->add_fields("workspace_id");
|
|
|
|
|
+ ws_index->set_type(::smartbotic::database::INDEX_TYPE_HASH);
|
|
|
|
|
+
|
|
|
|
|
+ auto* parent_index = create_req.add_indexes();
|
|
|
|
|
+ parent_index->set_collection(kArchivesCollection);
|
|
|
|
|
+ parent_index->set_index_name("parent_archive_id_idx");
|
|
|
|
|
+ parent_index->add_fields("parent_archive_id");
|
|
|
|
|
+ parent_index->set_type(::smartbotic::database::INDEX_TYPE_HASH);
|
|
|
|
|
+
|
|
|
|
|
+ auto* status_index = create_req.add_indexes();
|
|
|
|
|
+ status_index->set_collection(kArchivesCollection);
|
|
|
|
|
+ status_index->set_index_name("status_idx");
|
|
|
|
|
+ status_index->add_fields("status");
|
|
|
|
|
+ status_index->set_type(::smartbotic::database::INDEX_TYPE_HASH);
|
|
|
|
|
+
|
|
|
|
|
+ grpc::ClientContext create_ctx;
|
|
|
|
|
+ ::smartbotic::database::CollectionMetadata created_metadata;
|
|
|
|
|
+ status = collection_service->CreateCollection(&create_ctx, create_req, &created_metadata);
|
|
|
|
|
+
|
|
|
|
|
+ if (!status.ok()) {
|
|
|
|
|
+ spdlog::error("Failed to create {} collection: {}", kArchivesCollection, status.error_message());
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::info("Created system collection {}", kArchivesCollection);
|
|
|
|
|
+ initialized_ = true;
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void ArchiveService::SetServices(PageService* page_service,
|
|
|
|
|
+ ViewService* view_service,
|
|
|
|
|
+ CollectionService* collection_service,
|
|
|
|
|
+ DocumentService* document_service) {
|
|
|
|
|
+ page_service_ = page_service;
|
|
|
|
|
+ view_service_ = view_service;
|
|
|
|
|
+ collection_service_ = collection_service;
|
|
|
|
|
+ document_service_ = document_service;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::CreateArchive(const CreateArchiveRequest& request) -> ArchiveResult {
|
|
|
|
|
+ if (!initialized_) {
|
|
|
|
|
+ return {.success = false, .error = "Archive service not initialized", .archive = std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto* doc_service = db_client_.GetDocumentService();
|
|
|
|
|
+ if (doc_service == nullptr) {
|
|
|
|
|
+ return {.success = false, .error = "Document service not available", .archive = std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ArchiveInfo archive;
|
|
|
|
|
+ archive.entity_type = request.entity_type;
|
|
|
|
|
+ archive.entity_id = request.entity_id;
|
|
|
|
|
+ archive.workspace_id = request.workspace_id;
|
|
|
|
|
+ archive.parent_archive_id = request.parent_archive_id;
|
|
|
|
|
+ archive.data = request.data;
|
|
|
|
|
+ archive.children_count = 0;
|
|
|
|
|
+ archive.created_at = GetCurrentTimestamp();
|
|
|
|
|
+ archive.created_by = request.actor_id;
|
|
|
|
|
+ archive.expires_at = CalculateExpiration(request.retention_days);
|
|
|
|
|
+ archive.status = ArchiveStatus::Active;
|
|
|
|
|
+
|
|
|
|
|
+ ::smartbotic::database::CreateDocumentRequest create_req;
|
|
|
|
|
+ create_req.set_collection(kArchivesCollection);
|
|
|
|
|
+
|
|
|
|
|
+ auto* data = create_req.mutable_data();
|
|
|
|
|
+ SetStringValue(data, "entity_type", EntityTypeToString(archive.entity_type));
|
|
|
|
|
+ SetStringValue(data, "entity_id", archive.entity_id);
|
|
|
|
|
+ SetStringValue(data, "workspace_id", archive.workspace_id);
|
|
|
|
|
+ SetStringValue(data, "parent_archive_id", archive.parent_archive_id);
|
|
|
|
|
+ SetStringValue(data, "data", archive.data.dump());
|
|
|
|
|
+ SetIntValue(data, "children_count", archive.children_count);
|
|
|
|
|
+ SetStringValue(data, "created_at", archive.created_at);
|
|
|
|
|
+ SetStringValue(data, "created_by", archive.created_by);
|
|
|
|
|
+ SetStringValue(data, "expires_at", archive.expires_at);
|
|
|
|
|
+ SetStringValue(data, "status", ArchiveStatusToString(archive.status));
|
|
|
|
|
+ SetStringValue(data, "restored_at", "");
|
|
|
|
|
+ SetStringValue(data, "exported_at", "");
|
|
|
|
|
+
|
|
|
|
|
+ grpc::ClientContext ctx;
|
|
|
|
|
+ ::smartbotic::database::Document created_doc;
|
|
|
|
|
+ auto status = doc_service->CreateDocument(&ctx, create_req, &created_doc);
|
|
|
|
|
+
|
|
|
|
|
+ if (!status.ok()) {
|
|
|
|
|
+ spdlog::error("Failed to create archive: {}", status.error_message());
|
|
|
|
|
+ return {.success = false, .error = status.error_message(), .archive = std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ archive.id = created_doc.id();
|
|
|
|
|
+
|
|
|
|
|
+ // Update parent's children count if this is a child archive
|
|
|
|
|
+ if (!request.parent_archive_id.empty()) {
|
|
|
|
|
+ UpdateChildrenCount(request.parent_archive_id, 1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::info("Created archive {} for {} {}", archive.id,
|
|
|
|
|
+ EntityTypeToString(archive.entity_type), archive.entity_id);
|
|
|
|
|
+ return {.success = true, .error = "", .archive = archive};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::GetArchive(const std::string& id) -> ArchiveResult {
|
|
|
|
|
+ if (!initialized_) {
|
|
|
|
|
+ return {.success = false, .error = "Archive service not initialized", .archive = std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto* doc_service = db_client_.GetDocumentService();
|
|
|
|
|
+ if (doc_service == nullptr) {
|
|
|
|
|
+ return {.success = false, .error = "Document service not available", .archive = std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ::smartbotic::database::GetDocumentRequest get_req;
|
|
|
|
|
+ get_req.set_collection(kArchivesCollection);
|
|
|
|
|
+ get_req.set_id(id);
|
|
|
|
|
+
|
|
|
|
|
+ grpc::ClientContext ctx;
|
|
|
|
|
+ ::smartbotic::database::Document doc;
|
|
|
|
|
+ auto status = doc_service->GetDocument(&ctx, get_req, &doc);
|
|
|
|
|
+
|
|
|
|
|
+ if (!status.ok()) {
|
|
|
|
|
+ if (status.error_code() == grpc::StatusCode::NOT_FOUND) {
|
|
|
|
|
+ return {.success = false, .error = "Archive not found", .archive = std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+ return {.success = false, .error = status.error_message(), .archive = std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ArchiveInfo archive;
|
|
|
|
|
+ archive.id = doc.id();
|
|
|
|
|
+ archive.entity_type = StringToEntityType(GetStringValue(doc.data(), "entity_type"));
|
|
|
|
|
+ archive.entity_id = GetStringValue(doc.data(), "entity_id");
|
|
|
|
|
+ archive.workspace_id = GetStringValue(doc.data(), "workspace_id");
|
|
|
|
|
+ archive.parent_archive_id = GetStringValue(doc.data(), "parent_archive_id");
|
|
|
|
|
+ archive.children_count = GetIntValue(doc.data(), "children_count");
|
|
|
|
|
+ archive.created_at = GetStringValue(doc.data(), "created_at");
|
|
|
|
|
+ archive.created_by = GetStringValue(doc.data(), "created_by");
|
|
|
|
|
+ archive.expires_at = GetStringValue(doc.data(), "expires_at");
|
|
|
|
|
+ archive.status = StringToArchiveStatus(GetStringValue(doc.data(), "status"));
|
|
|
|
|
+ archive.restored_at = GetStringValue(doc.data(), "restored_at");
|
|
|
|
|
+ archive.exported_at = GetStringValue(doc.data(), "exported_at");
|
|
|
|
|
+
|
|
|
|
|
+ // Parse data JSON
|
|
|
|
|
+ std::string data_str = GetStringValue(doc.data(), "data");
|
|
|
|
|
+ if (!data_str.empty()) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ archive.data = nlohmann::json::parse(data_str);
|
|
|
|
|
+ } catch (const nlohmann::json::parse_error&) {
|
|
|
|
|
+ archive.data = nlohmann::json::object();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {.success = true, .error = "", .archive = archive};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::ListArchives(const ArchiveListRequest& request) -> ArchiveListResult {
|
|
|
|
|
+ if (!initialized_) {
|
|
|
|
|
+ return {.success = false, .error = "Archive service not initialized", .archives = {}, .total_count = 0};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto* query_service = db_client_.GetQueryService();
|
|
|
|
|
+ if (query_service == nullptr) {
|
|
|
|
|
+ return {.success = false, .error = "Query service not available", .archives = {}, .total_count = 0};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ::smartbotic::database::QueryRequest query_req;
|
|
|
|
|
+ query_req.set_collection(kArchivesCollection);
|
|
|
|
|
+ query_req.set_limit(request.limit);
|
|
|
|
|
+ query_req.set_offset(request.offset);
|
|
|
|
|
+
|
|
|
|
|
+ // Build composite filter
|
|
|
|
|
+ auto* filter = query_req.mutable_filter();
|
|
|
|
|
+ auto* composite = filter->mutable_composite();
|
|
|
|
|
+ composite->set_operator_(::smartbotic::database::COMPOSITE_OPERATOR_AND);
|
|
|
|
|
+
|
|
|
|
|
+ // Filter by workspace if specified
|
|
|
|
|
+ if (!request.workspace_id.empty()) {
|
|
|
|
|
+ auto* ws_filter = composite->add_filters()->mutable_field();
|
|
|
|
|
+ ws_filter->set_field("workspace_id");
|
|
|
|
|
+ ws_filter->set_operator_(::smartbotic::database::FILTER_OPERATOR_EQUAL);
|
|
|
|
|
+ ws_filter->mutable_value()->set_string_value(request.workspace_id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Filter by entity type if not including all types
|
|
|
|
|
+ if (!request.include_all_types) {
|
|
|
|
|
+ auto* type_filter = composite->add_filters()->mutable_field();
|
|
|
|
|
+ type_filter->set_field("entity_type");
|
|
|
|
|
+ type_filter->set_operator_(::smartbotic::database::FILTER_OPERATOR_EQUAL);
|
|
|
|
|
+ type_filter->mutable_value()->set_string_value(EntityTypeToString(request.entity_type));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Filter by parent archive ID
|
|
|
|
|
+ if (!request.parent_archive_id.empty()) {
|
|
|
|
|
+ auto* parent_filter = composite->add_filters()->mutable_field();
|
|
|
|
|
+ parent_filter->set_field("parent_archive_id");
|
|
|
|
|
+ parent_filter->set_operator_(::smartbotic::database::FILTER_OPERATOR_EQUAL);
|
|
|
|
|
+ parent_filter->mutable_value()->set_string_value(request.parent_archive_id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Filter by status (exclude expired unless requested)
|
|
|
|
|
+ if (!request.include_expired) {
|
|
|
|
|
+ auto* status_filter = composite->add_filters()->mutable_field();
|
|
|
|
|
+ status_filter->set_field("status");
|
|
|
|
|
+ status_filter->set_operator_(::smartbotic::database::FILTER_OPERATOR_EQUAL);
|
|
|
|
|
+ status_filter->mutable_value()->set_string_value("active");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Order by created_at descending
|
|
|
|
|
+ auto* order = query_req.add_order_by();
|
|
|
|
|
+ order->set_field("created_at");
|
|
|
|
|
+ order->set_direction(::smartbotic::database::SORT_DIRECTION_DESCENDING);
|
|
|
|
|
+
|
|
|
|
|
+ grpc::ClientContext ctx;
|
|
|
|
|
+ ::smartbotic::database::QueryResponse response;
|
|
|
|
|
+ auto status = query_service->Query(&ctx, query_req, &response);
|
|
|
|
|
+
|
|
|
|
|
+ if (!status.ok()) {
|
|
|
|
|
+ return {.success = false, .error = status.error_message(), .archives = {}, .total_count = 0};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ArchiveListResult result;
|
|
|
|
|
+ result.success = true;
|
|
|
|
|
+ result.total_count = response.total_count();
|
|
|
|
|
+
|
|
|
|
|
+ for (const auto& doc : response.documents()) {
|
|
|
|
|
+ ArchiveInfo archive;
|
|
|
|
|
+ archive.id = doc.id();
|
|
|
|
|
+ archive.entity_type = StringToEntityType(GetStringValue(doc.data(), "entity_type"));
|
|
|
|
|
+ archive.entity_id = GetStringValue(doc.data(), "entity_id");
|
|
|
|
|
+ archive.workspace_id = GetStringValue(doc.data(), "workspace_id");
|
|
|
|
|
+ archive.parent_archive_id = GetStringValue(doc.data(), "parent_archive_id");
|
|
|
|
|
+ archive.children_count = GetIntValue(doc.data(), "children_count");
|
|
|
|
|
+ archive.created_at = GetStringValue(doc.data(), "created_at");
|
|
|
|
|
+ archive.created_by = GetStringValue(doc.data(), "created_by");
|
|
|
|
|
+ archive.expires_at = GetStringValue(doc.data(), "expires_at");
|
|
|
|
|
+ archive.status = StringToArchiveStatus(GetStringValue(doc.data(), "status"));
|
|
|
|
|
+ archive.restored_at = GetStringValue(doc.data(), "restored_at");
|
|
|
|
|
+ archive.exported_at = GetStringValue(doc.data(), "exported_at");
|
|
|
|
|
+
|
|
|
|
|
+ // Parse data JSON
|
|
|
|
|
+ std::string data_str = GetStringValue(doc.data(), "data");
|
|
|
|
|
+ if (!data_str.empty()) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ archive.data = nlohmann::json::parse(data_str);
|
|
|
|
|
+ } catch (const nlohmann::json::parse_error&) {
|
|
|
|
|
+ archive.data = nlohmann::json::object();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ result.archives.push_back(archive);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::RestoreArchive(const std::string& archive_id,
|
|
|
|
|
+ const std::string& actor_id,
|
|
|
|
|
+ bool restore_children) -> RestoreResult {
|
|
|
|
|
+ // Get the archive
|
|
|
|
|
+ auto archive_result = GetArchive(archive_id);
|
|
|
|
|
+ if (!archive_result.success || !archive_result.archive) {
|
|
|
|
|
+ return {.success = false, .error = archive_result.error, .restored_entity_id = "", .restored_children_count = 0};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto& archive = *archive_result.archive;
|
|
|
|
|
+ if (archive.status != ArchiveStatus::Active) {
|
|
|
|
|
+ return {.success = false, .error = "Archive is not active", .restored_entity_id = "", .restored_children_count = 0};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ RestoreResult result;
|
|
|
|
|
+ result.success = true;
|
|
|
|
|
+ result.restored_children_count = 0;
|
|
|
|
|
+
|
|
|
|
|
+ // Restore based on entity type
|
|
|
|
|
+ switch (archive.entity_type) {
|
|
|
|
|
+ case EntityType::Workspace:
|
|
|
|
|
+ // Workspace restore would need to call workspace_service
|
|
|
|
|
+ // For now, return the data - caller can decide what to do
|
|
|
|
|
+ result.restored_entity_id = archive.entity_id;
|
|
|
|
|
+ break;
|
|
|
|
|
+
|
|
|
|
|
+ case EntityType::Page:
|
|
|
|
|
+ if (page_service_ != nullptr) {
|
|
|
|
|
+ // Create page from archived data
|
|
|
|
|
+ CreatePageRequest page_req;
|
|
|
|
|
+ page_req.workspace_id = archive.data.value("workspace_id", "");
|
|
|
|
|
+ page_req.name = archive.data.value("name", "");
|
|
|
|
|
+ page_req.slug = archive.data.value("slug", "");
|
|
|
|
|
+ page_req.created_by = actor_id;
|
|
|
|
|
+ // Parse layout and settings from archive.data if needed
|
|
|
|
|
+ auto page_result = page_service_->CreatePage(page_req);
|
|
|
|
|
+ if (page_result.success && page_result.page) {
|
|
|
|
|
+ result.restored_entity_id = page_result.page->id;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ result.success = false;
|
|
|
|
|
+ result.error = page_result.error;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ break;
|
|
|
|
|
+
|
|
|
|
|
+ case EntityType::View:
|
|
|
|
|
+ if (view_service_ != nullptr) {
|
|
|
|
|
+ CreateViewRequest view_req;
|
|
|
|
|
+ view_req.workspace_id = archive.data.value("workspace_id", "");
|
|
|
|
|
+ view_req.name = archive.data.value("name", "");
|
|
|
|
|
+ view_req.collection_name = archive.data.value("collection_name", "");
|
|
|
|
|
+ // Parse schema and settings from archive.data if needed
|
|
|
|
|
+ auto view_result = view_service_->CreateView(view_req);
|
|
|
|
|
+ if (view_result.success && view_result.view) {
|
|
|
|
|
+ result.restored_entity_id = view_result.view->id;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ result.success = false;
|
|
|
|
|
+ result.error = view_result.error;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ break;
|
|
|
|
|
+
|
|
|
|
|
+ case EntityType::Collection:
|
|
|
|
|
+ if (collection_service_ != nullptr) {
|
|
|
|
|
+ CreateCollectionRequest coll_req;
|
|
|
|
|
+ coll_req.workspace_id = archive.workspace_id;
|
|
|
|
|
+ coll_req.name = archive.entity_id; // Collection name is stored as entity_id
|
|
|
|
|
+ coll_req.actor_id = actor_id;
|
|
|
|
|
+ auto coll_result = collection_service_->CreateCollection(coll_req);
|
|
|
|
|
+ if (coll_result.success && coll_result.collection) {
|
|
|
|
|
+ result.restored_entity_id = coll_result.collection->name;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ result.success = false;
|
|
|
|
|
+ result.error = coll_result.error;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ break;
|
|
|
|
|
+
|
|
|
|
|
+ case EntityType::Document:
|
|
|
|
|
+ if (document_service_ != nullptr) {
|
|
|
|
|
+ webserver::CreateDocumentRequest doc_req;
|
|
|
|
|
+ doc_req.workspace_id = archive.workspace_id;
|
|
|
|
|
+ doc_req.collection = archive.data.value("collection", "");
|
|
|
|
|
+ doc_req.data = archive.data.value("data", nlohmann::json::object());
|
|
|
|
|
+ doc_req.user_id = actor_id;
|
|
|
|
|
+ auto doc_result = document_service_->CreateDocument(doc_req);
|
|
|
|
|
+ if (doc_result.success && doc_result.document) {
|
|
|
|
|
+ result.restored_entity_id = doc_result.document->id;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ result.success = false;
|
|
|
|
|
+ result.error = doc_result.error;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ break;
|
|
|
|
|
+
|
|
|
|
|
+ case EntityType::Session:
|
|
|
|
|
+ // Session restore would need LLM session service
|
|
|
|
|
+ result.restored_entity_id = archive.entity_id;
|
|
|
|
|
+ break;
|
|
|
|
|
+
|
|
|
|
|
+ case EntityType::User:
|
|
|
|
|
+ case EntityType::Group:
|
|
|
|
|
+ case EntityType::Membership:
|
|
|
|
|
+ // These types are not typically archived/restored via this service
|
|
|
|
|
+ result.restored_entity_id = archive.entity_id;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Restore children if requested
|
|
|
|
|
+ if (result.success && restore_children && archive.children_count > 0) {
|
|
|
|
|
+ auto children = GetChildArchives(archive_id);
|
|
|
|
|
+ for (const auto& child : children.archives) {
|
|
|
|
|
+ auto child_result = RestoreArchive(child.id, actor_id, true);
|
|
|
|
|
+ if (child_result.success) {
|
|
|
|
|
+ result.restored_children_count += 1 + child_result.restored_children_count;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Mark archive as restored
|
|
|
|
|
+ if (result.success) {
|
|
|
|
|
+ auto* doc_service = db_client_.GetDocumentService();
|
|
|
|
|
+ if (doc_service != nullptr) {
|
|
|
|
|
+ ::smartbotic::database::UpdateDocumentRequest update_req;
|
|
|
|
|
+ update_req.set_collection(kArchivesCollection);
|
|
|
|
|
+ update_req.set_id(archive_id);
|
|
|
|
|
+ update_req.set_merge(true);
|
|
|
|
|
+
|
|
|
|
|
+ auto* data = update_req.mutable_data();
|
|
|
|
|
+ SetStringValue(data, "status", ArchiveStatusToString(ArchiveStatus::Restored));
|
|
|
|
|
+ SetStringValue(data, "restored_at", GetCurrentTimestamp());
|
|
|
|
|
+
|
|
|
|
|
+ grpc::ClientContext ctx;
|
|
|
|
|
+ ::smartbotic::database::Document updated_doc;
|
|
|
|
|
+ doc_service->UpdateDocument(&ctx, update_req, &updated_doc);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::ExportArchive(const std::string& archive_id) -> ExportResult {
|
|
|
|
|
+ auto archive_result = GetArchive(archive_id);
|
|
|
|
|
+ if (!archive_result.success || !archive_result.archive) {
|
|
|
|
|
+ return {.success = false, .error = archive_result.error, .export_data = nlohmann::json::object()};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto& archive = *archive_result.archive;
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json export_data;
|
|
|
|
|
+ export_data["id"] = archive.id;
|
|
|
|
|
+ export_data["entity_type"] = EntityTypeToString(archive.entity_type);
|
|
|
|
|
+ export_data["entity_id"] = archive.entity_id;
|
|
|
|
|
+ export_data["workspace_id"] = archive.workspace_id;
|
|
|
|
|
+ export_data["data"] = archive.data;
|
|
|
|
|
+ export_data["created_at"] = archive.created_at;
|
|
|
|
|
+ export_data["created_by"] = archive.created_by;
|
|
|
|
|
+ export_data["exported_at"] = GetCurrentTimestamp();
|
|
|
|
|
+
|
|
|
|
|
+ // Include children
|
|
|
|
|
+ if (archive.children_count > 0) {
|
|
|
|
|
+ nlohmann::json children = nlohmann::json::array();
|
|
|
|
|
+ auto children_result = GetChildArchives(archive_id);
|
|
|
|
|
+ for (const auto& child : children_result.archives) {
|
|
|
|
|
+ auto child_export = ExportArchive(child.id);
|
|
|
|
|
+ if (child_export.success) {
|
|
|
|
|
+ children.push_back(child_export.export_data);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ export_data["children"] = children;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Mark archive as exported
|
|
|
|
|
+ auto* doc_service = db_client_.GetDocumentService();
|
|
|
|
|
+ if (doc_service != nullptr) {
|
|
|
|
|
+ ::smartbotic::database::UpdateDocumentRequest update_req;
|
|
|
|
|
+ update_req.set_collection(kArchivesCollection);
|
|
|
|
|
+ update_req.set_id(archive_id);
|
|
|
|
|
+ update_req.set_merge(true);
|
|
|
|
|
+
|
|
|
|
|
+ auto* data = update_req.mutable_data();
|
|
|
|
|
+ SetStringValue(data, "exported_at", GetCurrentTimestamp());
|
|
|
|
|
+
|
|
|
|
|
+ grpc::ClientContext ctx;
|
|
|
|
|
+ ::smartbotic::database::Document updated_doc;
|
|
|
|
|
+ doc_service->UpdateDocument(&ctx, update_req, &updated_doc);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {.success = true, .error = "", .export_data = export_data};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::DeleteArchive(const std::string& archive_id,
|
|
|
|
|
+ bool delete_children) -> ArchiveResult {
|
|
|
|
|
+ auto archive_result = GetArchive(archive_id);
|
|
|
|
|
+ if (!archive_result.success || !archive_result.archive) {
|
|
|
|
|
+ return archive_result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto& archive = *archive_result.archive;
|
|
|
|
|
+
|
|
|
|
|
+ // Delete children first if requested
|
|
|
|
|
+ if (delete_children && archive.children_count > 0) {
|
|
|
|
|
+ auto children = GetChildArchives(archive_id);
|
|
|
|
|
+ for (const auto& child : children.archives) {
|
|
|
|
|
+ (void)DeleteArchive(child.id, true);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Delete the archive
|
|
|
|
|
+ auto* doc_service = db_client_.GetDocumentService();
|
|
|
|
|
+ if (doc_service == nullptr) {
|
|
|
|
|
+ return {.success = false, .error = "Document service not available", .archive = std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ::smartbotic::database::DeleteDocumentRequest delete_req;
|
|
|
|
|
+ delete_req.set_collection(kArchivesCollection);
|
|
|
|
|
+ delete_req.set_id(archive_id);
|
|
|
|
|
+
|
|
|
|
|
+ grpc::ClientContext ctx;
|
|
|
|
|
+ ::smartbotic::database::DeleteDocumentResponse delete_resp;
|
|
|
|
|
+ auto status = doc_service->DeleteDocument(&ctx, delete_req, &delete_resp);
|
|
|
|
|
+
|
|
|
|
|
+ if (!status.ok()) {
|
|
|
|
|
+ return {.success = false, .error = status.error_message(), .archive = std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Update parent's children count if this had a parent
|
|
|
|
|
+ if (!archive.parent_archive_id.empty()) {
|
|
|
|
|
+ UpdateChildrenCount(archive.parent_archive_id, -1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::info("Deleted archive {} for {} {}", archive_id,
|
|
|
|
|
+ EntityTypeToString(archive.entity_type), archive.entity_id);
|
|
|
|
|
+ return {.success = true, .error = "", .archive = std::nullopt};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::CleanupExpiredArchives() -> CleanupResult {
|
|
|
|
|
+ if (!initialized_) {
|
|
|
|
|
+ return {.success = false, .error = "Archive service not initialized", .cleaned_count = 0};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // List all archives with expired status or past expiration date
|
|
|
|
|
+ ArchiveListRequest list_req;
|
|
|
|
|
+ list_req.include_all_types = true;
|
|
|
|
|
+ list_req.include_expired = true;
|
|
|
|
|
+ list_req.limit = 1000;
|
|
|
|
|
+
|
|
|
|
|
+ auto list_result = ListArchives(list_req);
|
|
|
|
|
+ if (!list_result.success) {
|
|
|
|
|
+ return {.success = false, .error = list_result.error, .cleaned_count = 0};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ int cleaned = 0;
|
|
|
|
|
+ for (const auto& archive : list_result.archives) {
|
|
|
|
|
+ // Check if expired
|
|
|
|
|
+ if (IsExpired(archive.expires_at) && archive.status == ArchiveStatus::Active) {
|
|
|
|
|
+ // Only delete top-level archives (they will cascade delete children)
|
|
|
|
|
+ if (archive.parent_archive_id.empty()) {
|
|
|
|
|
+ auto delete_result = DeleteArchive(archive.id, true);
|
|
|
|
|
+ if (delete_result.success) {
|
|
|
|
|
+ cleaned += 1 + static_cast<int>(archive.children_count);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::info("Cleaned up {} expired archives", cleaned);
|
|
|
|
|
+ return {.success = true, .error = "", .cleaned_count = cleaned};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::ArchiveWorkspace(const std::string& workspace_id,
|
|
|
|
|
+ const nlohmann::json& workspace_data,
|
|
|
|
|
+ const std::string& actor_id,
|
|
|
|
|
+ int retention_days) -> ArchiveResult {
|
|
|
|
|
+ // Create workspace archive
|
|
|
|
|
+ CreateArchiveRequest req;
|
|
|
|
|
+ req.entity_type = EntityType::Workspace;
|
|
|
|
|
+ req.entity_id = workspace_id;
|
|
|
|
|
+ req.workspace_id = workspace_id;
|
|
|
|
|
+ req.data = workspace_data;
|
|
|
|
|
+ req.actor_id = actor_id;
|
|
|
|
|
+ req.retention_days = retention_days;
|
|
|
|
|
+
|
|
|
|
|
+ auto ws_archive = CreateArchive(req);
|
|
|
|
|
+ if (!ws_archive.success) {
|
|
|
|
|
+ return ws_archive;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::string parent_archive_id = ws_archive.archive->id;
|
|
|
|
|
+
|
|
|
|
|
+ // Archive all pages
|
|
|
|
|
+ if (page_service_ != nullptr) {
|
|
|
|
|
+ auto pages_result = page_service_->ListPages(workspace_id);
|
|
|
|
|
+ if (pages_result.success) {
|
|
|
|
|
+ for (const auto& page : pages_result.pages) {
|
|
|
|
|
+ nlohmann::json page_data;
|
|
|
|
|
+ page_data["id"] = page.id;
|
|
|
|
|
+ page_data["workspace_id"] = page.workspace_id;
|
|
|
|
|
+ page_data["name"] = page.name;
|
|
|
|
|
+ page_data["slug"] = page.slug;
|
|
|
|
|
+ page_data["created_at"] = page.created_at;
|
|
|
|
|
+ page_data["updated_at"] = page.updated_at;
|
|
|
|
|
+ page_data["created_by"] = page.created_by;
|
|
|
|
|
+
|
|
|
|
|
+ (void)ArchivePage(page.id, page_data, workspace_id, parent_archive_id, actor_id, retention_days);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Archive all views
|
|
|
|
|
+ if (view_service_ != nullptr) {
|
|
|
|
|
+ auto views_result = view_service_->ListViews(workspace_id);
|
|
|
|
|
+ if (views_result.success) {
|
|
|
|
|
+ for (const auto& view : views_result.views) {
|
|
|
|
|
+ nlohmann::json view_data;
|
|
|
|
|
+ view_data["id"] = view.id;
|
|
|
|
|
+ view_data["workspace_id"] = view.workspace_id;
|
|
|
|
|
+ view_data["name"] = view.name;
|
|
|
|
|
+ view_data["collection_name"] = view.collection_name;
|
|
|
|
|
+ view_data["created_at"] = view.created_at;
|
|
|
|
|
+ view_data["updated_at"] = view.updated_at;
|
|
|
|
|
+
|
|
|
|
|
+ (void)ArchiveView(view.id, view_data, workspace_id, parent_archive_id, actor_id, retention_days);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Archive all collections and their documents
|
|
|
|
|
+ if (collection_service_ != nullptr) {
|
|
|
|
|
+ auto collections_result = collection_service_->ListCollections(workspace_id);
|
|
|
|
|
+ if (collections_result.success) {
|
|
|
|
|
+ for (const auto& coll : collections_result.collections) {
|
|
|
|
|
+ nlohmann::json coll_data;
|
|
|
|
|
+ coll_data["name"] = coll.name;
|
|
|
|
|
+ coll_data["workspace_id"] = coll.workspace_id;
|
|
|
|
|
+ coll_data["document_count"] = coll.document_count;
|
|
|
|
|
+
|
|
|
|
|
+ auto coll_archive = ArchiveCollection(coll.name, coll_data, workspace_id,
|
|
|
|
|
+ parent_archive_id, actor_id, retention_days);
|
|
|
|
|
+
|
|
|
|
|
+ // Archive documents in this collection
|
|
|
|
|
+ if (coll_archive.success && document_service_ != nullptr) {
|
|
|
|
|
+ DocumentQuery doc_query;
|
|
|
|
|
+ doc_query.workspace_id = workspace_id;
|
|
|
|
|
+ doc_query.collection = coll.name;
|
|
|
|
|
+ doc_query.limit = 10000; // Get all documents
|
|
|
|
|
+
|
|
|
|
|
+ auto docs_result = document_service_->ListDocuments(doc_query);
|
|
|
|
|
+ if (docs_result.success) {
|
|
|
|
|
+ for (const auto& doc : docs_result.documents) {
|
|
|
|
|
+ nlohmann::json doc_data;
|
|
|
|
|
+ doc_data["id"] = doc.id;
|
|
|
|
|
+ doc_data["collection"] = doc.collection;
|
|
|
|
|
+ doc_data["data"] = doc.data;
|
|
|
|
|
+ doc_data["created_at"] = doc.created_at;
|
|
|
|
|
+ doc_data["updated_at"] = doc.updated_at;
|
|
|
|
|
+
|
|
|
|
|
+ (void)ArchiveDocument(doc.id, doc_data, coll.name, workspace_id,
|
|
|
|
|
+ coll_archive.archive->id, actor_id, retention_days);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return ws_archive;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::ArchivePage(const std::string& page_id,
|
|
|
|
|
+ const nlohmann::json& page_data,
|
|
|
|
|
+ const std::string& workspace_id,
|
|
|
|
|
+ const std::string& parent_archive_id,
|
|
|
|
|
+ const std::string& actor_id,
|
|
|
|
|
+ int retention_days) -> ArchiveResult {
|
|
|
|
|
+ CreateArchiveRequest req;
|
|
|
|
|
+ req.entity_type = EntityType::Page;
|
|
|
|
|
+ req.entity_id = page_id;
|
|
|
|
|
+ req.workspace_id = workspace_id;
|
|
|
|
|
+ req.parent_archive_id = parent_archive_id;
|
|
|
|
|
+ req.data = page_data;
|
|
|
|
|
+ req.actor_id = actor_id;
|
|
|
|
|
+ req.retention_days = retention_days;
|
|
|
|
|
+ return CreateArchive(req);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::ArchiveView(const std::string& view_id,
|
|
|
|
|
+ const nlohmann::json& view_data,
|
|
|
|
|
+ const std::string& workspace_id,
|
|
|
|
|
+ const std::string& parent_archive_id,
|
|
|
|
|
+ const std::string& actor_id,
|
|
|
|
|
+ int retention_days) -> ArchiveResult {
|
|
|
|
|
+ CreateArchiveRequest req;
|
|
|
|
|
+ req.entity_type = EntityType::View;
|
|
|
|
|
+ req.entity_id = view_id;
|
|
|
|
|
+ req.workspace_id = workspace_id;
|
|
|
|
|
+ req.parent_archive_id = parent_archive_id;
|
|
|
|
|
+ req.data = view_data;
|
|
|
|
|
+ req.actor_id = actor_id;
|
|
|
|
|
+ req.retention_days = retention_days;
|
|
|
|
|
+ return CreateArchive(req);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::ArchiveCollection(const std::string& collection_name,
|
|
|
|
|
+ const nlohmann::json& collection_data,
|
|
|
|
|
+ const std::string& workspace_id,
|
|
|
|
|
+ const std::string& parent_archive_id,
|
|
|
|
|
+ const std::string& actor_id,
|
|
|
|
|
+ int retention_days) -> ArchiveResult {
|
|
|
|
|
+ CreateArchiveRequest req;
|
|
|
|
|
+ req.entity_type = EntityType::Collection;
|
|
|
|
|
+ req.entity_id = collection_name;
|
|
|
|
|
+ req.workspace_id = workspace_id;
|
|
|
|
|
+ req.parent_archive_id = parent_archive_id;
|
|
|
|
|
+ req.data = collection_data;
|
|
|
|
|
+ req.actor_id = actor_id;
|
|
|
|
|
+ req.retention_days = retention_days;
|
|
|
|
|
+ return CreateArchive(req);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::ArchiveDocument(const std::string& document_id,
|
|
|
|
|
+ const nlohmann::json& document_data,
|
|
|
|
|
+ const std::string& collection_name,
|
|
|
|
|
+ const std::string& workspace_id,
|
|
|
|
|
+ const std::string& parent_archive_id,
|
|
|
|
|
+ const std::string& actor_id,
|
|
|
|
|
+ int retention_days) -> ArchiveResult {
|
|
|
|
|
+ CreateArchiveRequest req;
|
|
|
|
|
+ req.entity_type = EntityType::Document;
|
|
|
|
|
+ req.entity_id = document_id;
|
|
|
|
|
+ req.workspace_id = workspace_id;
|
|
|
|
|
+ req.parent_archive_id = parent_archive_id;
|
|
|
|
|
+ req.data = document_data;
|
|
|
|
|
+ req.actor_id = actor_id;
|
|
|
|
|
+ req.retention_days = retention_days;
|
|
|
|
|
+ return CreateArchive(req);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::ArchiveSession(const std::string& session_id,
|
|
|
|
|
+ const nlohmann::json& session_data,
|
|
|
|
|
+ const std::string& workspace_id,
|
|
|
|
|
+ const std::string& parent_archive_id,
|
|
|
|
|
+ const std::string& actor_id,
|
|
|
|
|
+ int retention_days) -> ArchiveResult {
|
|
|
|
|
+ CreateArchiveRequest req;
|
|
|
|
|
+ req.entity_type = EntityType::Session;
|
|
|
|
|
+ req.entity_id = session_id;
|
|
|
|
|
+ req.workspace_id = workspace_id;
|
|
|
|
|
+ req.parent_archive_id = parent_archive_id;
|
|
|
|
|
+ req.data = session_data;
|
|
|
|
|
+ req.actor_id = actor_id;
|
|
|
|
|
+ req.retention_days = retention_days;
|
|
|
|
|
+ return CreateArchive(req);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::ArchiveToJson(const ArchiveInfo& archive) -> nlohmann::json {
|
|
|
|
|
+ return {
|
|
|
|
|
+ {"id", archive.id},
|
|
|
|
|
+ {"entity_type", EntityTypeToString(archive.entity_type)},
|
|
|
|
|
+ {"entity_id", archive.entity_id},
|
|
|
|
|
+ {"workspace_id", archive.workspace_id},
|
|
|
|
|
+ {"parent_archive_id", archive.parent_archive_id},
|
|
|
|
|
+ {"data", archive.data},
|
|
|
|
|
+ {"children_count", archive.children_count},
|
|
|
|
|
+ {"created_at", archive.created_at},
|
|
|
|
|
+ {"created_by", archive.created_by},
|
|
|
|
|
+ {"expires_at", archive.expires_at},
|
|
|
|
|
+ {"status", ArchiveStatusToString(archive.status)},
|
|
|
|
|
+ {"restored_at", archive.restored_at},
|
|
|
|
|
+ {"exported_at", archive.exported_at}
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::JsonToArchive(const nlohmann::json& json) -> ArchiveInfo {
|
|
|
|
|
+ ArchiveInfo archive;
|
|
|
|
|
+ archive.id = json.value("id", "");
|
|
|
|
|
+ archive.entity_type = StringToEntityType(json.value("entity_type", "workspace"));
|
|
|
|
|
+ archive.entity_id = json.value("entity_id", "");
|
|
|
|
|
+ archive.workspace_id = json.value("workspace_id", "");
|
|
|
|
|
+ archive.parent_archive_id = json.value("parent_archive_id", "");
|
|
|
|
|
+ archive.data = json.value("data", nlohmann::json::object());
|
|
|
|
|
+ archive.children_count = json.value("children_count", 0);
|
|
|
|
|
+ archive.created_at = json.value("created_at", "");
|
|
|
|
|
+ archive.created_by = json.value("created_by", "");
|
|
|
|
|
+ archive.expires_at = json.value("expires_at", "");
|
|
|
|
|
+ archive.status = StringToArchiveStatus(json.value("status", "active"));
|
|
|
|
|
+ archive.restored_at = json.value("restored_at", "");
|
|
|
|
|
+ archive.exported_at = json.value("exported_at", "");
|
|
|
|
|
+ return archive;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::GetCurrentTimestamp() -> std::string {
|
|
|
|
|
+ auto now = std::chrono::system_clock::now();
|
|
|
|
|
+ auto time = std::chrono::system_clock::to_time_t(now);
|
|
|
|
|
+ auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;
|
|
|
|
|
+
|
|
|
|
|
+ std::tm tm{};
|
|
|
|
|
+ gmtime_r(&time, &tm);
|
|
|
|
|
+
|
|
|
|
|
+ std::ostringstream oss;
|
|
|
|
|
+ oss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S");
|
|
|
|
|
+ oss << '.' << std::setfill('0') << std::setw(3) << ms.count() << 'Z';
|
|
|
|
|
+ return oss.str();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::CalculateExpiration(int retention_days) -> std::string {
|
|
|
|
|
+ auto now = std::chrono::system_clock::now();
|
|
|
|
|
+ auto expiration = now + std::chrono::hours(24 * retention_days);
|
|
|
|
|
+ auto time = std::chrono::system_clock::to_time_t(expiration);
|
|
|
|
|
+
|
|
|
|
|
+ std::tm tm{};
|
|
|
|
|
+ gmtime_r(&time, &tm);
|
|
|
|
|
+
|
|
|
|
|
+ std::ostringstream oss;
|
|
|
|
|
+ oss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S");
|
|
|
|
|
+ oss << ".000Z";
|
|
|
|
|
+ return oss.str();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::IsExpired(const std::string& expires_at) -> bool {
|
|
|
|
|
+ if (expires_at.empty()) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Parse the expiration timestamp
|
|
|
|
|
+ std::tm tm{};
|
|
|
|
|
+ std::istringstream ss(expires_at);
|
|
|
|
|
+ ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S");
|
|
|
|
|
+
|
|
|
|
|
+ if (ss.fail()) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto expiration_time = std::chrono::system_clock::from_time_t(timegm(&tm));
|
|
|
|
|
+ auto now = std::chrono::system_clock::now();
|
|
|
|
|
+
|
|
|
|
|
+ return now > expiration_time;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::UpdateChildrenCount(const std::string& archive_id, int64_t delta) -> bool {
|
|
|
|
|
+ auto archive_result = GetArchive(archive_id);
|
|
|
|
|
+ if (!archive_result.success || !archive_result.archive) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto* doc_service = db_client_.GetDocumentService();
|
|
|
|
|
+ if (doc_service == nullptr) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ::smartbotic::database::UpdateDocumentRequest update_req;
|
|
|
|
|
+ update_req.set_collection(kArchivesCollection);
|
|
|
|
|
+ update_req.set_id(archive_id);
|
|
|
|
|
+ update_req.set_merge(true);
|
|
|
|
|
+
|
|
|
|
|
+ auto* data = update_req.mutable_data();
|
|
|
|
|
+ SetIntValue(data, "children_count", archive_result.archive->children_count + delta);
|
|
|
|
|
+
|
|
|
|
|
+ grpc::ClientContext ctx;
|
|
|
|
|
+ ::smartbotic::database::Document updated_doc;
|
|
|
|
|
+ auto status = doc_service->UpdateDocument(&ctx, update_req, &updated_doc);
|
|
|
|
|
+
|
|
|
|
|
+ return status.ok();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ArchiveService::GetChildArchives(const std::string& parent_archive_id) -> ArchiveListResult {
|
|
|
|
|
+ ArchiveListRequest req;
|
|
|
|
|
+ req.parent_archive_id = parent_archive_id;
|
|
|
|
|
+ req.include_all_types = true;
|
|
|
|
|
+ req.limit = 10000; // Get all children
|
|
|
|
|
+ return ListArchives(req);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+} // namespace smartbotic::webserver
|