| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591 |
- #include "smartbotic/webserver/view_service.hpp"
- #include <chrono>
- #include <iomanip>
- #include <sstream>
- #include <spdlog/spdlog.h>
- 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<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();
- }
- } // 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<std::string>());
- } else if (it.value().is_boolean()) {
- value.set_bool_value(it.value().get<bool>());
- } 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<ViewInfo> 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<ViewInfo> 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<std::string>());
- } else if (it.value().is_boolean()) {
- value.set_bool_value(it.value().get<bool>());
- } 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<std::string>());
- }
- }
- }
- 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<std::string>());
- }
- }
- }
- return settings;
- }
- } // namespace smartbotic::webserver
|