#include "smartbotic/webserver/view_service.hpp" #include #include #include #include namespace smartbotic::webserver { auto FieldTypeToString(FieldType type) -> std::string { switch (type) { case FieldType::Text: return "text"; case FieldType::Number: return "number"; case FieldType::Date: return "date"; case FieldType::Boolean: return "boolean"; case FieldType::Select: return "select"; case FieldType::Reference: return "reference"; case FieldType::Computed: return "computed"; } return "text"; } auto StringToFieldType(const std::string& str) -> FieldType { if (str == "number") return FieldType::Number; if (str == "date") return FieldType::Date; if (str == "boolean") return FieldType::Boolean; if (str == "select") return FieldType::Select; if (str == "reference") return FieldType::Reference; if (str == "computed") return FieldType::Computed; return FieldType::Text; } namespace { /// Get current timestamp as ISO 8601 string auto 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( 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(); } } // namespace ViewService::ViewService(DatabaseClient& db_client) : db_client_(db_client) {} ViewService::~ViewService() = default; ViewService::ViewService(ViewService&&) noexcept = default; auto ViewService::operator=(ViewService&& /*other*/) noexcept -> ViewService& { // Cannot reassign reference member return *this; } auto ViewService::Initialize() -> bool { // Check if _views collection exists, create if not grpc::ClientContext ctx; ::smartbotic::database::GetCollectionMetadataRequest req; ::smartbotic::database::CollectionMetadata resp; req.set_name(kViewsCollection); auto status = db_client_.GetCollectionService()->GetCollectionMetadata(&ctx, req, &resp); if (status.ok()) { spdlog::info("System collection {} already exists", kViewsCollection); return true; } if (status.error_code() == grpc::StatusCode::NOT_FOUND) { // Create the collection grpc::ClientContext create_ctx; ::smartbotic::database::CreateCollectionRequest create_req; ::smartbotic::database::CollectionMetadata create_resp; create_req.set_name(kViewsCollection); auto create_status = db_client_.GetCollectionService()->CreateCollection( &create_ctx, create_req, &create_resp); if (!create_status.ok()) { spdlog::error("Failed to create system collection {}: {}", kViewsCollection, create_status.error_message()); return false; } spdlog::info("Created system collection {}", kViewsCollection); return true; } spdlog::error("Failed to check system collection {}: {}", kViewsCollection, status.error_message()); return false; } auto ViewService::CreateView(const CreateViewRequest& request) -> ViewResult { // Check if view with same name already exists in workspace auto existing = GetViewByName(request.workspace_id, request.name); if (existing.success && existing.view) { return {.success = false, .error = "View with this name already exists", .view = std::nullopt}; } // Create view document ViewInfo view; view.workspace_id = request.workspace_id; view.name = request.name; view.collection_name = request.collection_name; view.schema = request.schema; view.settings = request.settings; view.created_at = GetCurrentTimestamp(); view.updated_at = view.created_at; auto json_data = ViewToJson(view); grpc::ClientContext ctx; ::smartbotic::database::CreateDocumentRequest req; ::smartbotic::database::Document resp; req.set_collection(kViewsCollection); // Convert JSON to MapValue auto* data = req.mutable_data(); for (auto it = json_data.begin(); it != json_data.end(); ++it) { ::smartbotic::database::Value value; if (it.value().is_string()) { value.set_string_value(it.value().get()); } else if (it.value().is_boolean()) { value.set_bool_value(it.value().get()); } else if (it.value().is_object() || it.value().is_array()) { value.set_string_value(it.value().dump()); // Store complex types as JSON string } (*data->mutable_fields())[it.key()] = value; } auto status = db_client_.GetDocumentService()->CreateDocument(&ctx, req, &resp); if (!status.ok()) { spdlog::error("Failed to create view {}: {}", request.name, status.error_message()); return {.success = false, .error = "Failed to create view: " + status.error_message(), .view = std::nullopt}; } view.id = resp.id(); spdlog::info("Created view {} in workspace {}", view.name, view.workspace_id); return {.success = true, .error = "", .view = view}; } auto ViewService::GetView(const std::string& workspace_id, const std::string& id) -> ViewResult { grpc::ClientContext ctx; ::smartbotic::database::GetDocumentRequest req; ::smartbotic::database::Document resp; req.set_collection(kViewsCollection); req.set_id(id); auto status = db_client_.GetDocumentService()->GetDocument(&ctx, req, &resp); if (!status.ok()) { if (status.error_code() == grpc::StatusCode::NOT_FOUND) { return {.success = false, .error = "View not found", .view = std::nullopt}; } return {.success = false, .error = "Failed to get view: " + status.error_message(), .view = std::nullopt}; } // Convert response to JSON nlohmann::json json_data; for (const auto& [key, value] : resp.data().fields()) { if (value.has_string_value()) { // Try to parse as JSON if it looks like JSON const auto& str = value.string_value(); if ((str.starts_with('{') && str.ends_with('}')) || (str.starts_with('[') && str.ends_with(']'))) { try { json_data[key] = nlohmann::json::parse(str); } catch (...) { json_data[key] = str; } } else { json_data[key] = str; } } else if (value.has_bool_value()) { json_data[key] = value.bool_value(); } else if (value.has_int_value()) { json_data[key] = value.int_value(); } } json_data["id"] = resp.id(); auto view = JsonToView(json_data); // Verify workspace matches if (view.workspace_id != workspace_id) { return {.success = false, .error = "View not found", .view = std::nullopt}; } return {.success = true, .error = "", .view = view}; } auto ViewService::GetViewByName(const std::string& workspace_id, const std::string& name) -> ViewResult { grpc::ClientContext ctx; ::smartbotic::database::QueryRequest req; ::smartbotic::database::QueryResponse resp; req.set_collection(kViewsCollection); req.set_limit(1); // Build filter for workspace_id and name auto* filter = req.mutable_filter(); auto* composite = filter->mutable_composite(); composite->set_operator_(::smartbotic::database::COMPOSITE_OPERATOR_AND); 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(workspace_id); auto* name_filter = composite->add_filters()->mutable_field(); name_filter->set_field("name"); name_filter->set_operator_(::smartbotic::database::FILTER_OPERATOR_EQUAL); name_filter->mutable_value()->set_string_value(name); auto status = db_client_.GetQueryService()->Query(&ctx, req, &resp); if (!status.ok()) { return {.success = false, .error = "Failed to query views: " + status.error_message(), .view = std::nullopt}; } if (resp.documents_size() == 0) { return {.success = false, .error = "View not found", .view = std::nullopt}; } // Convert first document const auto& doc = resp.documents(0); nlohmann::json json_data; for (const auto& [key, value] : doc.data().fields()) { if (value.has_string_value()) { const auto& str = value.string_value(); if ((str.starts_with('{') && str.ends_with('}')) || (str.starts_with('[') && str.ends_with(']'))) { try { json_data[key] = nlohmann::json::parse(str); } catch (...) { json_data[key] = str; } } else { json_data[key] = str; } } else if (value.has_bool_value()) { json_data[key] = value.bool_value(); } } json_data["id"] = doc.id(); auto view = JsonToView(json_data); return {.success = true, .error = "", .view = view}; } auto ViewService::ListViews(const std::string& workspace_id) -> ViewListResult { grpc::ClientContext ctx; ::smartbotic::database::QueryRequest req; ::smartbotic::database::QueryResponse resp; req.set_collection(kViewsCollection); req.set_limit(1000); // Filter by workspace_id auto* filter = req.mutable_filter()->mutable_field(); filter->set_field("workspace_id"); filter->set_operator_(::smartbotic::database::FILTER_OPERATOR_EQUAL); filter->mutable_value()->set_string_value(workspace_id); auto status = db_client_.GetQueryService()->Query(&ctx, req, &resp); if (!status.ok()) { return {.success = false, .error = "Failed to list views: " + status.error_message(), .views = {}}; } std::vector views; views.reserve(resp.documents_size()); for (const auto& doc : resp.documents()) { nlohmann::json json_data; for (const auto& [key, value] : doc.data().fields()) { if (value.has_string_value()) { const auto& str = value.string_value(); if ((str.starts_with('{') && str.ends_with('}')) || (str.starts_with('[') && str.ends_with(']'))) { try { json_data[key] = nlohmann::json::parse(str); } catch (...) { json_data[key] = str; } } else { json_data[key] = str; } } else if (value.has_bool_value()) { json_data[key] = value.bool_value(); } } json_data["id"] = doc.id(); views.push_back(JsonToView(json_data)); } return {.success = true, .error = "", .views = std::move(views)}; } auto ViewService::ListViewsForCollection(const std::string& workspace_id, const std::string& collection_name) -> ViewListResult { grpc::ClientContext ctx; ::smartbotic::database::QueryRequest req; ::smartbotic::database::QueryResponse resp; req.set_collection(kViewsCollection); req.set_limit(1000); // Filter by workspace_id and collection_name auto* filter = req.mutable_filter(); auto* composite = filter->mutable_composite(); composite->set_operator_(::smartbotic::database::COMPOSITE_OPERATOR_AND); 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(workspace_id); auto* coll_filter = composite->add_filters()->mutable_field(); coll_filter->set_field("collection_name"); coll_filter->set_operator_(::smartbotic::database::FILTER_OPERATOR_EQUAL); coll_filter->mutable_value()->set_string_value(collection_name); auto status = db_client_.GetQueryService()->Query(&ctx, req, &resp); if (!status.ok()) { return {.success = false, .error = "Failed to list views: " + status.error_message(), .views = {}}; } std::vector views; views.reserve(resp.documents_size()); for (const auto& doc : resp.documents()) { nlohmann::json json_data; for (const auto& [key, value] : doc.data().fields()) { if (value.has_string_value()) { const auto& str = value.string_value(); if ((str.starts_with('{') && str.ends_with('}')) || (str.starts_with('[') && str.ends_with(']'))) { try { json_data[key] = nlohmann::json::parse(str); } catch (...) { json_data[key] = str; } } else { json_data[key] = str; } } else if (value.has_bool_value()) { json_data[key] = value.bool_value(); } } json_data["id"] = doc.id(); views.push_back(JsonToView(json_data)); } return {.success = true, .error = "", .views = std::move(views)}; } auto ViewService::UpdateView(const UpdateViewRequest& request) -> ViewResult { // Get existing view auto existing = GetView(request.workspace_id, request.id); if (!existing.success || !existing.view) { return {.success = false, .error = "View not found", .view = std::nullopt}; } ViewInfo updated = *existing.view; // Apply updates if (request.name) { // Check for name conflict if (*request.name != updated.name) { auto conflict = GetViewByName(request.workspace_id, *request.name); if (conflict.success && conflict.view) { return {.success = false, .error = "View with this name already exists", .view = std::nullopt}; } } updated.name = *request.name; } if (request.collection_name) { updated.collection_name = *request.collection_name; } if (request.schema) { updated.schema = *request.schema; } if (request.settings) { updated.settings = *request.settings; } updated.updated_at = GetCurrentTimestamp(); auto json_data = ViewToJson(updated); grpc::ClientContext ctx; ::smartbotic::database::UpdateDocumentRequest req; ::smartbotic::database::Document resp; req.set_collection(kViewsCollection); req.set_id(request.id); req.set_merge(false); // Replace entire document // Convert JSON to MapValue auto* data = req.mutable_data(); for (auto it = json_data.begin(); it != json_data.end(); ++it) { ::smartbotic::database::Value value; if (it.value().is_string()) { value.set_string_value(it.value().get()); } else if (it.value().is_boolean()) { value.set_bool_value(it.value().get()); } else if (it.value().is_object() || it.value().is_array()) { value.set_string_value(it.value().dump()); } (*data->mutable_fields())[it.key()] = value; } auto status = db_client_.GetDocumentService()->UpdateDocument(&ctx, req, &resp); if (!status.ok()) { spdlog::error("Failed to update view {}: {}", request.id, status.error_message()); return {.success = false, .error = "Failed to update view: " + status.error_message(), .view = std::nullopt}; } spdlog::info("Updated view {} in workspace {}", updated.name, updated.workspace_id); return {.success = true, .error = "", .view = updated}; } auto ViewService::DeleteView(const std::string& workspace_id, const std::string& id) -> ViewResult { // Verify view exists and belongs to workspace auto existing = GetView(workspace_id, id); if (!existing.success || !existing.view) { return {.success = false, .error = "View not found", .view = std::nullopt}; } grpc::ClientContext ctx; ::smartbotic::database::DeleteDocumentRequest req; ::smartbotic::database::DeleteDocumentResponse resp; req.set_collection(kViewsCollection); req.set_id(id); auto status = db_client_.GetDocumentService()->DeleteDocument(&ctx, req, &resp); if (!status.ok()) { spdlog::error("Failed to delete view {}: {}", id, status.error_message()); return {.success = false, .error = "Failed to delete view: " + status.error_message(), .view = std::nullopt}; } spdlog::info("Deleted view {} from workspace {}", existing.view->name, workspace_id); return {.success = true, .error = "", .view = std::nullopt}; } auto ViewService::ViewToJson(const ViewInfo& view) -> nlohmann::json { return { {"workspace_id", view.workspace_id}, {"name", view.name}, {"collection_name", view.collection_name}, {"schema", SchemaToJson(view.schema)}, {"settings", SettingsToJson(view.settings)}, {"created_at", view.created_at}, {"updated_at", view.updated_at} }; } auto ViewService::JsonToView(const nlohmann::json& json) -> ViewInfo { ViewInfo view; view.id = json.value("id", ""); view.workspace_id = json.value("workspace_id", ""); view.name = json.value("name", ""); view.collection_name = json.value("collection_name", ""); view.created_at = json.value("created_at", ""); view.updated_at = json.value("updated_at", ""); if (json.contains("schema") && json["schema"].is_object()) { view.schema = JsonToSchema(json["schema"]); } if (json.contains("settings") && json["settings"].is_object()) { view.settings = JsonToSettings(json["settings"]); } return view; } auto ViewService::SchemaToJson(const ViewSchema& schema) -> nlohmann::json { nlohmann::json fields_json = nlohmann::json::array(); for (const auto& field : schema.fields) { nlohmann::json field_json = { {"name", field.name}, {"type", FieldTypeToString(field.type)}, {"label", field.label}, {"description", field.description}, {"required", field.required}, {"display_order", field.display_order}, {"widget", field.widget}, {"group", field.group} }; if (!field.default_value.is_null()) { field_json["default_value"] = field.default_value; } if (!field.options.is_null()) { field_json["options"] = field.options; } if (!field.reference_collection.empty()) { field_json["reference_collection"] = field.reference_collection; } if (!field.computed_expression.empty()) { field_json["computed_expression"] = field.computed_expression; } fields_json.push_back(field_json); } return { {"fields", fields_json}, {"title", schema.title}, {"description", schema.description}, {"layout", schema.layout} }; } auto ViewService::JsonToSchema(const nlohmann::json& json) -> ViewSchema { ViewSchema schema; schema.title = json.value("title", ""); schema.description = json.value("description", ""); schema.layout = json.value("layout", nlohmann::json::object()); if (json.contains("fields") && json["fields"].is_array()) { for (const auto& field_json : json["fields"]) { SchemaField field; field.name = field_json.value("name", ""); field.type = StringToFieldType(field_json.value("type", "text")); field.label = field_json.value("label", ""); field.description = field_json.value("description", ""); field.required = field_json.value("required", false); field.display_order = field_json.value("display_order", 0); field.widget = field_json.value("widget", ""); field.group = field_json.value("group", ""); field.default_value = field_json.value("default_value", nlohmann::json()); field.options = field_json.value("options", nlohmann::json()); field.reference_collection = field_json.value("reference_collection", ""); field.computed_expression = field_json.value("computed_expression", ""); schema.fields.push_back(field); } } return schema; } auto ViewService::SettingsToJson(const ViewSettings& settings) -> nlohmann::json { return { {"is_default", settings.is_default}, {"show_in_sidebar", settings.show_in_sidebar}, {"icon", settings.icon}, {"filters", settings.filters}, {"sort", settings.sort}, {"visible_to_groups", settings.visible_to_groups}, {"editable_by_groups", settings.editable_by_groups} }; } auto ViewService::JsonToSettings(const nlohmann::json& json) -> ViewSettings { ViewSettings settings; settings.is_default = json.value("is_default", false); settings.show_in_sidebar = json.value("show_in_sidebar", true); settings.icon = json.value("icon", ""); settings.filters = json.value("filters", nlohmann::json::object()); settings.sort = json.value("sort", nlohmann::json::object()); // Parse access control arrays if (json.contains("visible_to_groups") && json["visible_to_groups"].is_array()) { for (const auto& group : json["visible_to_groups"]) { if (group.is_string()) { settings.visible_to_groups.push_back(group.get()); } } } if (json.contains("editable_by_groups") && json["editable_by_groups"].is_array()) { for (const auto& group : json["editable_by_groups"]) { if (group.is_string()) { settings.editable_by_groups.push_back(group.get()); } } } return settings; } } // namespace smartbotic::webserver