view_service.cpp 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. #include "smartbotic/webserver/view_service.hpp"
  2. #include <chrono>
  3. #include <iomanip>
  4. #include <sstream>
  5. #include <spdlog/spdlog.h>
  6. namespace smartbotic::webserver {
  7. auto FieldTypeToString(FieldType type) -> std::string {
  8. switch (type) {
  9. case FieldType::Text: return "text";
  10. case FieldType::Number: return "number";
  11. case FieldType::Date: return "date";
  12. case FieldType::Boolean: return "boolean";
  13. case FieldType::Select: return "select";
  14. case FieldType::Reference: return "reference";
  15. case FieldType::Computed: return "computed";
  16. }
  17. return "text";
  18. }
  19. auto StringToFieldType(const std::string& str) -> FieldType {
  20. if (str == "number") return FieldType::Number;
  21. if (str == "date") return FieldType::Date;
  22. if (str == "boolean") return FieldType::Boolean;
  23. if (str == "select") return FieldType::Select;
  24. if (str == "reference") return FieldType::Reference;
  25. if (str == "computed") return FieldType::Computed;
  26. return FieldType::Text;
  27. }
  28. namespace {
  29. /// Get current timestamp as ISO 8601 string
  30. auto GetCurrentTimestamp() -> std::string {
  31. auto now = std::chrono::system_clock::now();
  32. auto time = std::chrono::system_clock::to_time_t(now);
  33. auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
  34. now.time_since_epoch()) % 1000;
  35. std::tm tm{};
  36. gmtime_r(&time, &tm);
  37. std::ostringstream oss;
  38. oss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S");
  39. oss << "." << std::setfill('0') << std::setw(3) << ms.count() << "Z";
  40. return oss.str();
  41. }
  42. } // namespace
  43. ViewService::ViewService(DatabaseClient& db_client) : db_client_(db_client) {}
  44. ViewService::~ViewService() = default;
  45. ViewService::ViewService(ViewService&&) noexcept = default;
  46. auto ViewService::operator=(ViewService&& /*other*/) noexcept -> ViewService& {
  47. // Cannot reassign reference member
  48. return *this;
  49. }
  50. auto ViewService::Initialize() -> bool {
  51. // Check if _views collection exists, create if not
  52. grpc::ClientContext ctx;
  53. ::smartbotic::database::GetCollectionMetadataRequest req;
  54. ::smartbotic::database::CollectionMetadata resp;
  55. req.set_name(kViewsCollection);
  56. auto status = db_client_.GetCollectionService()->GetCollectionMetadata(&ctx, req, &resp);
  57. if (status.ok()) {
  58. spdlog::info("System collection {} already exists", kViewsCollection);
  59. return true;
  60. }
  61. if (status.error_code() == grpc::StatusCode::NOT_FOUND) {
  62. // Create the collection
  63. grpc::ClientContext create_ctx;
  64. ::smartbotic::database::CreateCollectionRequest create_req;
  65. ::smartbotic::database::CollectionMetadata create_resp;
  66. create_req.set_name(kViewsCollection);
  67. auto create_status = db_client_.GetCollectionService()->CreateCollection(
  68. &create_ctx, create_req, &create_resp);
  69. if (!create_status.ok()) {
  70. spdlog::error("Failed to create system collection {}: {}",
  71. kViewsCollection, create_status.error_message());
  72. return false;
  73. }
  74. spdlog::info("Created system collection {}", kViewsCollection);
  75. return true;
  76. }
  77. spdlog::error("Failed to check system collection {}: {}",
  78. kViewsCollection, status.error_message());
  79. return false;
  80. }
  81. auto ViewService::CreateView(const CreateViewRequest& request) -> ViewResult {
  82. // Check if view with same name already exists in workspace
  83. auto existing = GetViewByName(request.workspace_id, request.name);
  84. if (existing.success && existing.view) {
  85. return {.success = false, .error = "View with this name already exists", .view = std::nullopt};
  86. }
  87. // Create view document
  88. ViewInfo view;
  89. view.workspace_id = request.workspace_id;
  90. view.name = request.name;
  91. view.collection_name = request.collection_name;
  92. view.schema = request.schema;
  93. view.settings = request.settings;
  94. view.created_at = GetCurrentTimestamp();
  95. view.updated_at = view.created_at;
  96. auto json_data = ViewToJson(view);
  97. grpc::ClientContext ctx;
  98. ::smartbotic::database::CreateDocumentRequest req;
  99. ::smartbotic::database::Document resp;
  100. req.set_collection(kViewsCollection);
  101. // Convert JSON to MapValue
  102. auto* data = req.mutable_data();
  103. for (auto it = json_data.begin(); it != json_data.end(); ++it) {
  104. ::smartbotic::database::Value value;
  105. if (it.value().is_string()) {
  106. value.set_string_value(it.value().get<std::string>());
  107. } else if (it.value().is_boolean()) {
  108. value.set_bool_value(it.value().get<bool>());
  109. } else if (it.value().is_object() || it.value().is_array()) {
  110. value.set_string_value(it.value().dump()); // Store complex types as JSON string
  111. }
  112. (*data->mutable_fields())[it.key()] = value;
  113. }
  114. auto status = db_client_.GetDocumentService()->CreateDocument(&ctx, req, &resp);
  115. if (!status.ok()) {
  116. spdlog::error("Failed to create view {}: {}", request.name, status.error_message());
  117. return {.success = false, .error = "Failed to create view: " + status.error_message(), .view = std::nullopt};
  118. }
  119. view.id = resp.id();
  120. spdlog::info("Created view {} in workspace {}", view.name, view.workspace_id);
  121. return {.success = true, .error = "", .view = view};
  122. }
  123. auto ViewService::GetView(const std::string& workspace_id, const std::string& id) -> ViewResult {
  124. grpc::ClientContext ctx;
  125. ::smartbotic::database::GetDocumentRequest req;
  126. ::smartbotic::database::Document resp;
  127. req.set_collection(kViewsCollection);
  128. req.set_id(id);
  129. auto status = db_client_.GetDocumentService()->GetDocument(&ctx, req, &resp);
  130. if (!status.ok()) {
  131. if (status.error_code() == grpc::StatusCode::NOT_FOUND) {
  132. return {.success = false, .error = "View not found", .view = std::nullopt};
  133. }
  134. return {.success = false, .error = "Failed to get view: " + status.error_message(), .view = std::nullopt};
  135. }
  136. // Convert response to JSON
  137. nlohmann::json json_data;
  138. for (const auto& [key, value] : resp.data().fields()) {
  139. if (value.has_string_value()) {
  140. // Try to parse as JSON if it looks like JSON
  141. const auto& str = value.string_value();
  142. if ((str.starts_with('{') && str.ends_with('}')) ||
  143. (str.starts_with('[') && str.ends_with(']'))) {
  144. try {
  145. json_data[key] = nlohmann::json::parse(str);
  146. } catch (...) {
  147. json_data[key] = str;
  148. }
  149. } else {
  150. json_data[key] = str;
  151. }
  152. } else if (value.has_bool_value()) {
  153. json_data[key] = value.bool_value();
  154. } else if (value.has_int_value()) {
  155. json_data[key] = value.int_value();
  156. }
  157. }
  158. json_data["id"] = resp.id();
  159. auto view = JsonToView(json_data);
  160. // Verify workspace matches
  161. if (view.workspace_id != workspace_id) {
  162. return {.success = false, .error = "View not found", .view = std::nullopt};
  163. }
  164. return {.success = true, .error = "", .view = view};
  165. }
  166. auto ViewService::GetViewByName(const std::string& workspace_id,
  167. const std::string& name) -> ViewResult {
  168. grpc::ClientContext ctx;
  169. ::smartbotic::database::QueryRequest req;
  170. ::smartbotic::database::QueryResponse resp;
  171. req.set_collection(kViewsCollection);
  172. req.set_limit(1);
  173. // Build filter for workspace_id and name
  174. auto* filter = req.mutable_filter();
  175. auto* composite = filter->mutable_composite();
  176. composite->set_operator_(::smartbotic::database::COMPOSITE_OPERATOR_AND);
  177. auto* ws_filter = composite->add_filters()->mutable_field();
  178. ws_filter->set_field("workspace_id");
  179. ws_filter->set_operator_(::smartbotic::database::FILTER_OPERATOR_EQUAL);
  180. ws_filter->mutable_value()->set_string_value(workspace_id);
  181. auto* name_filter = composite->add_filters()->mutable_field();
  182. name_filter->set_field("name");
  183. name_filter->set_operator_(::smartbotic::database::FILTER_OPERATOR_EQUAL);
  184. name_filter->mutable_value()->set_string_value(name);
  185. auto status = db_client_.GetQueryService()->Query(&ctx, req, &resp);
  186. if (!status.ok()) {
  187. return {.success = false, .error = "Failed to query views: " + status.error_message(), .view = std::nullopt};
  188. }
  189. if (resp.documents_size() == 0) {
  190. return {.success = false, .error = "View not found", .view = std::nullopt};
  191. }
  192. // Convert first document
  193. const auto& doc = resp.documents(0);
  194. nlohmann::json json_data;
  195. for (const auto& [key, value] : doc.data().fields()) {
  196. if (value.has_string_value()) {
  197. const auto& str = value.string_value();
  198. if ((str.starts_with('{') && str.ends_with('}')) ||
  199. (str.starts_with('[') && str.ends_with(']'))) {
  200. try {
  201. json_data[key] = nlohmann::json::parse(str);
  202. } catch (...) {
  203. json_data[key] = str;
  204. }
  205. } else {
  206. json_data[key] = str;
  207. }
  208. } else if (value.has_bool_value()) {
  209. json_data[key] = value.bool_value();
  210. }
  211. }
  212. json_data["id"] = doc.id();
  213. auto view = JsonToView(json_data);
  214. return {.success = true, .error = "", .view = view};
  215. }
  216. auto ViewService::ListViews(const std::string& workspace_id) -> ViewListResult {
  217. grpc::ClientContext ctx;
  218. ::smartbotic::database::QueryRequest req;
  219. ::smartbotic::database::QueryResponse resp;
  220. req.set_collection(kViewsCollection);
  221. req.set_limit(1000);
  222. // Filter by workspace_id
  223. auto* filter = req.mutable_filter()->mutable_field();
  224. filter->set_field("workspace_id");
  225. filter->set_operator_(::smartbotic::database::FILTER_OPERATOR_EQUAL);
  226. filter->mutable_value()->set_string_value(workspace_id);
  227. auto status = db_client_.GetQueryService()->Query(&ctx, req, &resp);
  228. if (!status.ok()) {
  229. return {.success = false, .error = "Failed to list views: " + status.error_message(), .views = {}};
  230. }
  231. std::vector<ViewInfo> views;
  232. views.reserve(resp.documents_size());
  233. for (const auto& doc : resp.documents()) {
  234. nlohmann::json json_data;
  235. for (const auto& [key, value] : doc.data().fields()) {
  236. if (value.has_string_value()) {
  237. const auto& str = value.string_value();
  238. if ((str.starts_with('{') && str.ends_with('}')) ||
  239. (str.starts_with('[') && str.ends_with(']'))) {
  240. try {
  241. json_data[key] = nlohmann::json::parse(str);
  242. } catch (...) {
  243. json_data[key] = str;
  244. }
  245. } else {
  246. json_data[key] = str;
  247. }
  248. } else if (value.has_bool_value()) {
  249. json_data[key] = value.bool_value();
  250. }
  251. }
  252. json_data["id"] = doc.id();
  253. views.push_back(JsonToView(json_data));
  254. }
  255. return {.success = true, .error = "", .views = std::move(views)};
  256. }
  257. auto ViewService::ListViewsForCollection(const std::string& workspace_id,
  258. const std::string& collection_name) -> ViewListResult {
  259. grpc::ClientContext ctx;
  260. ::smartbotic::database::QueryRequest req;
  261. ::smartbotic::database::QueryResponse resp;
  262. req.set_collection(kViewsCollection);
  263. req.set_limit(1000);
  264. // Filter by workspace_id and collection_name
  265. auto* filter = req.mutable_filter();
  266. auto* composite = filter->mutable_composite();
  267. composite->set_operator_(::smartbotic::database::COMPOSITE_OPERATOR_AND);
  268. auto* ws_filter = composite->add_filters()->mutable_field();
  269. ws_filter->set_field("workspace_id");
  270. ws_filter->set_operator_(::smartbotic::database::FILTER_OPERATOR_EQUAL);
  271. ws_filter->mutable_value()->set_string_value(workspace_id);
  272. auto* coll_filter = composite->add_filters()->mutable_field();
  273. coll_filter->set_field("collection_name");
  274. coll_filter->set_operator_(::smartbotic::database::FILTER_OPERATOR_EQUAL);
  275. coll_filter->mutable_value()->set_string_value(collection_name);
  276. auto status = db_client_.GetQueryService()->Query(&ctx, req, &resp);
  277. if (!status.ok()) {
  278. return {.success = false, .error = "Failed to list views: " + status.error_message(), .views = {}};
  279. }
  280. std::vector<ViewInfo> views;
  281. views.reserve(resp.documents_size());
  282. for (const auto& doc : resp.documents()) {
  283. nlohmann::json json_data;
  284. for (const auto& [key, value] : doc.data().fields()) {
  285. if (value.has_string_value()) {
  286. const auto& str = value.string_value();
  287. if ((str.starts_with('{') && str.ends_with('}')) ||
  288. (str.starts_with('[') && str.ends_with(']'))) {
  289. try {
  290. json_data[key] = nlohmann::json::parse(str);
  291. } catch (...) {
  292. json_data[key] = str;
  293. }
  294. } else {
  295. json_data[key] = str;
  296. }
  297. } else if (value.has_bool_value()) {
  298. json_data[key] = value.bool_value();
  299. }
  300. }
  301. json_data["id"] = doc.id();
  302. views.push_back(JsonToView(json_data));
  303. }
  304. return {.success = true, .error = "", .views = std::move(views)};
  305. }
  306. auto ViewService::UpdateView(const UpdateViewRequest& request) -> ViewResult {
  307. // Get existing view
  308. auto existing = GetView(request.workspace_id, request.id);
  309. if (!existing.success || !existing.view) {
  310. return {.success = false, .error = "View not found", .view = std::nullopt};
  311. }
  312. ViewInfo updated = *existing.view;
  313. // Apply updates
  314. if (request.name) {
  315. // Check for name conflict
  316. if (*request.name != updated.name) {
  317. auto conflict = GetViewByName(request.workspace_id, *request.name);
  318. if (conflict.success && conflict.view) {
  319. return {.success = false, .error = "View with this name already exists", .view = std::nullopt};
  320. }
  321. }
  322. updated.name = *request.name;
  323. }
  324. if (request.collection_name) {
  325. updated.collection_name = *request.collection_name;
  326. }
  327. if (request.schema) {
  328. updated.schema = *request.schema;
  329. }
  330. if (request.settings) {
  331. updated.settings = *request.settings;
  332. }
  333. updated.updated_at = GetCurrentTimestamp();
  334. auto json_data = ViewToJson(updated);
  335. grpc::ClientContext ctx;
  336. ::smartbotic::database::UpdateDocumentRequest req;
  337. ::smartbotic::database::Document resp;
  338. req.set_collection(kViewsCollection);
  339. req.set_id(request.id);
  340. req.set_merge(false); // Replace entire document
  341. // Convert JSON to MapValue
  342. auto* data = req.mutable_data();
  343. for (auto it = json_data.begin(); it != json_data.end(); ++it) {
  344. ::smartbotic::database::Value value;
  345. if (it.value().is_string()) {
  346. value.set_string_value(it.value().get<std::string>());
  347. } else if (it.value().is_boolean()) {
  348. value.set_bool_value(it.value().get<bool>());
  349. } else if (it.value().is_object() || it.value().is_array()) {
  350. value.set_string_value(it.value().dump());
  351. }
  352. (*data->mutable_fields())[it.key()] = value;
  353. }
  354. auto status = db_client_.GetDocumentService()->UpdateDocument(&ctx, req, &resp);
  355. if (!status.ok()) {
  356. spdlog::error("Failed to update view {}: {}", request.id, status.error_message());
  357. return {.success = false, .error = "Failed to update view: " + status.error_message(), .view = std::nullopt};
  358. }
  359. spdlog::info("Updated view {} in workspace {}", updated.name, updated.workspace_id);
  360. return {.success = true, .error = "", .view = updated};
  361. }
  362. auto ViewService::DeleteView(const std::string& workspace_id, const std::string& id) -> ViewResult {
  363. // Verify view exists and belongs to workspace
  364. auto existing = GetView(workspace_id, id);
  365. if (!existing.success || !existing.view) {
  366. return {.success = false, .error = "View not found", .view = std::nullopt};
  367. }
  368. grpc::ClientContext ctx;
  369. ::smartbotic::database::DeleteDocumentRequest req;
  370. ::smartbotic::database::DeleteDocumentResponse resp;
  371. req.set_collection(kViewsCollection);
  372. req.set_id(id);
  373. auto status = db_client_.GetDocumentService()->DeleteDocument(&ctx, req, &resp);
  374. if (!status.ok()) {
  375. spdlog::error("Failed to delete view {}: {}", id, status.error_message());
  376. return {.success = false, .error = "Failed to delete view: " + status.error_message(), .view = std::nullopt};
  377. }
  378. spdlog::info("Deleted view {} from workspace {}", existing.view->name, workspace_id);
  379. return {.success = true, .error = "", .view = std::nullopt};
  380. }
  381. auto ViewService::ViewToJson(const ViewInfo& view) -> nlohmann::json {
  382. return {
  383. {"workspace_id", view.workspace_id},
  384. {"name", view.name},
  385. {"collection_name", view.collection_name},
  386. {"schema", SchemaToJson(view.schema)},
  387. {"settings", SettingsToJson(view.settings)},
  388. {"created_at", view.created_at},
  389. {"updated_at", view.updated_at}
  390. };
  391. }
  392. auto ViewService::JsonToView(const nlohmann::json& json) -> ViewInfo {
  393. ViewInfo view;
  394. view.id = json.value("id", "");
  395. view.workspace_id = json.value("workspace_id", "");
  396. view.name = json.value("name", "");
  397. view.collection_name = json.value("collection_name", "");
  398. view.created_at = json.value("created_at", "");
  399. view.updated_at = json.value("updated_at", "");
  400. if (json.contains("schema") && json["schema"].is_object()) {
  401. view.schema = JsonToSchema(json["schema"]);
  402. }
  403. if (json.contains("settings") && json["settings"].is_object()) {
  404. view.settings = JsonToSettings(json["settings"]);
  405. }
  406. return view;
  407. }
  408. auto ViewService::SchemaToJson(const ViewSchema& schema) -> nlohmann::json {
  409. nlohmann::json fields_json = nlohmann::json::array();
  410. for (const auto& field : schema.fields) {
  411. nlohmann::json field_json = {
  412. {"name", field.name},
  413. {"type", FieldTypeToString(field.type)},
  414. {"label", field.label},
  415. {"description", field.description},
  416. {"required", field.required},
  417. {"display_order", field.display_order},
  418. {"widget", field.widget},
  419. {"group", field.group}
  420. };
  421. if (!field.default_value.is_null()) {
  422. field_json["default_value"] = field.default_value;
  423. }
  424. if (!field.options.is_null()) {
  425. field_json["options"] = field.options;
  426. }
  427. if (!field.reference_collection.empty()) {
  428. field_json["reference_collection"] = field.reference_collection;
  429. }
  430. if (!field.computed_expression.empty()) {
  431. field_json["computed_expression"] = field.computed_expression;
  432. }
  433. fields_json.push_back(field_json);
  434. }
  435. return {
  436. {"fields", fields_json},
  437. {"title", schema.title},
  438. {"description", schema.description},
  439. {"layout", schema.layout}
  440. };
  441. }
  442. auto ViewService::JsonToSchema(const nlohmann::json& json) -> ViewSchema {
  443. ViewSchema schema;
  444. schema.title = json.value("title", "");
  445. schema.description = json.value("description", "");
  446. schema.layout = json.value("layout", nlohmann::json::object());
  447. if (json.contains("fields") && json["fields"].is_array()) {
  448. for (const auto& field_json : json["fields"]) {
  449. SchemaField field;
  450. field.name = field_json.value("name", "");
  451. field.type = StringToFieldType(field_json.value("type", "text"));
  452. field.label = field_json.value("label", "");
  453. field.description = field_json.value("description", "");
  454. field.required = field_json.value("required", false);
  455. field.display_order = field_json.value("display_order", 0);
  456. field.widget = field_json.value("widget", "");
  457. field.group = field_json.value("group", "");
  458. field.default_value = field_json.value("default_value", nlohmann::json());
  459. field.options = field_json.value("options", nlohmann::json());
  460. field.reference_collection = field_json.value("reference_collection", "");
  461. field.computed_expression = field_json.value("computed_expression", "");
  462. schema.fields.push_back(field);
  463. }
  464. }
  465. return schema;
  466. }
  467. auto ViewService::SettingsToJson(const ViewSettings& settings) -> nlohmann::json {
  468. return {
  469. {"is_default", settings.is_default},
  470. {"show_in_sidebar", settings.show_in_sidebar},
  471. {"icon", settings.icon},
  472. {"filters", settings.filters},
  473. {"sort", settings.sort},
  474. {"visible_to_groups", settings.visible_to_groups},
  475. {"editable_by_groups", settings.editable_by_groups}
  476. };
  477. }
  478. auto ViewService::JsonToSettings(const nlohmann::json& json) -> ViewSettings {
  479. ViewSettings settings;
  480. settings.is_default = json.value("is_default", false);
  481. settings.show_in_sidebar = json.value("show_in_sidebar", true);
  482. settings.icon = json.value("icon", "");
  483. settings.filters = json.value("filters", nlohmann::json::object());
  484. settings.sort = json.value("sort", nlohmann::json::object());
  485. // Parse access control arrays
  486. if (json.contains("visible_to_groups") && json["visible_to_groups"].is_array()) {
  487. for (const auto& group : json["visible_to_groups"]) {
  488. if (group.is_string()) {
  489. settings.visible_to_groups.push_back(group.get<std::string>());
  490. }
  491. }
  492. }
  493. if (json.contains("editable_by_groups") && json["editable_by_groups"].is_array()) {
  494. for (const auto& group : json["editable_by_groups"]) {
  495. if (group.is_string()) {
  496. settings.editable_by_groups.push_back(group.get<std::string>());
  497. }
  498. }
  499. }
  500. return settings;
  501. }
  502. } // namespace smartbotic::webserver