|
|
@@ -0,0 +1,194 @@
|
|
|
+#include "view_manager.hpp"
|
|
|
+
|
|
|
+#include "../memory_store.hpp"
|
|
|
+
|
|
|
+#include <chrono>
|
|
|
+#include <nlohmann/json.hpp>
|
|
|
+#include <spdlog/spdlog.h>
|
|
|
+
|
|
|
+namespace smartbotic::database {
|
|
|
+
|
|
|
+namespace {
|
|
|
+uint64_t nowMs() {
|
|
|
+ return std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
|
+ std::chrono::system_clock::now().time_since_epoch()).count();
|
|
|
+}
|
|
|
+
|
|
|
+nlohmann::json filterToJson(const Filter& f) {
|
|
|
+ return f.toJson();
|
|
|
+}
|
|
|
+
|
|
|
+Filter filterFromJson(const nlohmann::json& j) {
|
|
|
+ return Filter::fromJson(j);
|
|
|
+}
|
|
|
+
|
|
|
+nlohmann::json sortToJson(const Sort& s) {
|
|
|
+ return s.toJson();
|
|
|
+}
|
|
|
+
|
|
|
+Sort sortFromJson(const nlohmann::json& j) {
|
|
|
+ return Sort::fromJson(j);
|
|
|
+}
|
|
|
+
|
|
|
+nlohmann::json toJson(const ViewInfo& v) {
|
|
|
+ nlohmann::json j = {
|
|
|
+ {"name", v.name},
|
|
|
+ {"collection", v.collection},
|
|
|
+ {"include", v.include},
|
|
|
+ {"exclude", v.exclude},
|
|
|
+ {"created_at", v.createdAt},
|
|
|
+ {"updated_at", v.updatedAt}
|
|
|
+ };
|
|
|
+ nlohmann::json whereArr = nlohmann::json::array();
|
|
|
+ for (const auto& f : v.where) whereArr.push_back(filterToJson(f));
|
|
|
+ j["where"] = whereArr;
|
|
|
+ if (v.defaultSort) j["default_sort"] = sortToJson(*v.defaultSort);
|
|
|
+ return j;
|
|
|
+}
|
|
|
+
|
|
|
+ViewInfo fromJson(const nlohmann::json& j) {
|
|
|
+ ViewInfo v;
|
|
|
+ v.name = j.value("name", "");
|
|
|
+ v.collection = j.value("collection", "");
|
|
|
+ if (j.contains("include") && j["include"].is_array()) {
|
|
|
+ for (const auto& p : j["include"]) v.include.push_back(p.get<std::string>());
|
|
|
+ }
|
|
|
+ if (j.contains("exclude") && j["exclude"].is_array()) {
|
|
|
+ for (const auto& p : j["exclude"]) v.exclude.push_back(p.get<std::string>());
|
|
|
+ }
|
|
|
+ if (j.contains("where") && j["where"].is_array()) {
|
|
|
+ for (const auto& f : j["where"]) v.where.push_back(filterFromJson(f));
|
|
|
+ }
|
|
|
+ if (j.contains("default_sort") && j["default_sort"].is_object()) {
|
|
|
+ v.defaultSort = sortFromJson(j["default_sort"]);
|
|
|
+ }
|
|
|
+ v.createdAt = j.value("created_at", uint64_t{0});
|
|
|
+ v.updatedAt = j.value("updated_at", uint64_t{0});
|
|
|
+ return v;
|
|
|
+}
|
|
|
+
|
|
|
+} // anonymous namespace
|
|
|
+
|
|
|
+ViewManager::ViewManager(MemoryStore& store) : store_(store) {}
|
|
|
+
|
|
|
+void ViewManager::loadFromStore() {
|
|
|
+ std::unique_lock<std::shared_mutex> lock(cacheMutex_);
|
|
|
+ cache_.clear();
|
|
|
+
|
|
|
+ // Ensure _views collection exists
|
|
|
+ CollectionOptions opts;
|
|
|
+ store_.createCollection(SYSTEM_COLLECTION, opts);
|
|
|
+
|
|
|
+ // Load all view definitions
|
|
|
+ Query q;
|
|
|
+ auto result = store_.find(SYSTEM_COLLECTION, q);
|
|
|
+ for (const auto& doc : result.documents) {
|
|
|
+ ViewInfo v = fromJson(doc.data);
|
|
|
+ if (v.name.empty()) continue;
|
|
|
+ cache_[v.name] = v;
|
|
|
+ }
|
|
|
+ spdlog::info("ViewManager: loaded {} views from {}", cache_.size(), SYSTEM_COLLECTION);
|
|
|
+}
|
|
|
+
|
|
|
+bool ViewManager::createView(const ViewInfo& view, std::string& errorOut) {
|
|
|
+ if (view.name.empty()) {
|
|
|
+ errorOut = "view name is required";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (view.collection.empty()) {
|
|
|
+ errorOut = "target collection is required";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (view.name == view.collection) {
|
|
|
+ errorOut = "view name cannot equal collection name";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!view.name.empty() && view.name[0] == '_') {
|
|
|
+ errorOut = "view name cannot start with '_' (reserved for system collections)";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // No view-of-view, no duplicate
|
|
|
+ {
|
|
|
+ std::shared_lock<std::shared_mutex> rlock(cacheMutex_);
|
|
|
+ if (cache_.contains(view.collection)) {
|
|
|
+ errorOut = "target '" + view.collection + "' is a view — views can only target real collections";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (cache_.contains(view.name)) {
|
|
|
+ errorOut = "view '" + view.name + "' already exists";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ViewInfo v = view;
|
|
|
+ v.createdAt = nowMs();
|
|
|
+ v.updatedAt = v.createdAt;
|
|
|
+
|
|
|
+ // Persist to _views (view name is the document ID)
|
|
|
+ Document doc;
|
|
|
+ doc.id = v.name;
|
|
|
+ doc.data = toJson(v);
|
|
|
+ try {
|
|
|
+ std::string id = store_.insert(SYSTEM_COLLECTION, doc);
|
|
|
+ if (id.empty()) {
|
|
|
+ errorOut = "failed to persist view definition";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ } catch (const std::exception& e) {
|
|
|
+ errorOut = std::string("failed to persist view definition: ") + e.what();
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ std::unique_lock<std::shared_mutex> wlock(cacheMutex_);
|
|
|
+ cache_[v.name] = v;
|
|
|
+ }
|
|
|
+ spdlog::info("ViewManager: created view '{}' over '{}'", v.name, v.collection);
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+bool ViewManager::dropView(const std::string& name, std::string& errorOut) {
|
|
|
+ {
|
|
|
+ std::shared_lock<std::shared_mutex> rlock(cacheMutex_);
|
|
|
+ if (!cache_.contains(name)) {
|
|
|
+ errorOut = "view '" + name + "' does not exist";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ bool removed = store_.remove(SYSTEM_COLLECTION, name);
|
|
|
+ if (!removed) {
|
|
|
+ errorOut = "failed to remove view from store";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ std::unique_lock<std::shared_mutex> wlock(cacheMutex_);
|
|
|
+ cache_.erase(name);
|
|
|
+ }
|
|
|
+ spdlog::info("ViewManager: dropped view '{}'", name);
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+bool ViewManager::isView(const std::string& name) const {
|
|
|
+ std::shared_lock<std::shared_mutex> lock(cacheMutex_);
|
|
|
+ return cache_.contains(name);
|
|
|
+}
|
|
|
+
|
|
|
+std::optional<ViewInfo> ViewManager::getView(const std::string& name) const {
|
|
|
+ std::shared_lock<std::shared_mutex> lock(cacheMutex_);
|
|
|
+ auto it = cache_.find(name);
|
|
|
+ if (it == cache_.end()) return std::nullopt;
|
|
|
+ return it->second;
|
|
|
+}
|
|
|
+
|
|
|
+std::vector<ViewInfo> ViewManager::listViews() const {
|
|
|
+ std::shared_lock<std::shared_mutex> lock(cacheMutex_);
|
|
|
+ std::vector<ViewInfo> out;
|
|
|
+ out.reserve(cache_.size());
|
|
|
+ for (const auto& [_, v] : cache_) out.push_back(v);
|
|
|
+ return out;
|
|
|
+}
|
|
|
+
|
|
|
+} // namespace smartbotic::database
|