Procházet zdrojové kódy

feat(views): add create_view migration operation

Supports include, exclude, where (with all operators — EQ/NE/GT/GTE/LT/
LTE/IN/CONTAINS/EXISTS/REGEX/SEARCH — via string or integer op field),
and default_sort (field + descending). Idempotent: 'already exists'
treated as success on migration replay.
fszontagh před 3 měsíci
rodič
revize
df30a6fea5

+ 1 - 1
service/src/database_service.cpp

@@ -78,7 +78,7 @@ bool DatabaseService::runMigrations() {
     migrationConfig.autoApply = config_.migrations.autoApply;
     migrationConfig.failOnError = config_.migrations.failOnError;
 
-    migrationRunner_ = std::make_unique<MigrationRunner>(*store_, migrationConfig);
+    migrationRunner_ = std::make_unique<MigrationRunner>(*store_, *view_manager_, migrationConfig);
     return migrationRunner_->runMigrations();
 }
 

+ 88 - 1
service/src/migrations/migration_runner.cpp

@@ -11,8 +11,9 @@
 
 namespace smartbotic::database {
 
-MigrationRunner::MigrationRunner(MemoryStore& store, Config config)
+MigrationRunner::MigrationRunner(MemoryStore& store, ViewManager& view_manager, Config config)
     : store_(store)
+    , view_manager_(view_manager)
     , config_(std::move(config))
 {
     // Ensure migrations collection exists
@@ -254,6 +255,92 @@ bool MigrationRunner::applyOperation(const nlohmann::json& operation) {
         return true;
     }
 
+    if (type == "create_view") {
+        std::string name = operation.value("name", "");
+        std::string collection = operation.value("collection", "");
+        if (name.empty() || collection.empty()) {
+            spdlog::error("create_view: missing name or collection");
+            return false;
+        }
+
+        ViewInfo v;
+        v.name = name;
+        v.collection = collection;
+
+        if (operation.contains("include") && operation["include"].is_array()) {
+            for (const auto& p : operation["include"]) {
+                v.include.push_back(p.get<std::string>());
+            }
+        }
+        if (operation.contains("exclude") && operation["exclude"].is_array()) {
+            for (const auto& p : operation["exclude"]) {
+                v.exclude.push_back(p.get<std::string>());
+            }
+        }
+
+        // Parse where filters
+        if (operation.contains("where") && operation["where"].is_array()) {
+            for (const auto& fj : operation["where"]) {
+                Filter f;
+                f.field = fj.value("field", "");
+
+                // Op can be a string name ("EQ", "GT", ...) or integer
+                if (fj.contains("op")) {
+                    const auto& opJson = fj["op"];
+                    if (opJson.is_string()) {
+                        std::string opStr = opJson.get<std::string>();
+                        if      (opStr == "EQ")       f.op = FilterOp::EQ;
+                        else if (opStr == "NE")       f.op = FilterOp::NE;
+                        else if (opStr == "GT")       f.op = FilterOp::GT;
+                        else if (opStr == "GTE")      f.op = FilterOp::GTE;
+                        else if (opStr == "LT")       f.op = FilterOp::LT;
+                        else if (opStr == "LTE")      f.op = FilterOp::LTE;
+                        else if (opStr == "IN")       f.op = FilterOp::IN;
+                        else if (opStr == "CONTAINS") f.op = FilterOp::CONTAINS;
+                        else if (opStr == "EXISTS")   f.op = FilterOp::EXISTS;
+                        else if (opStr == "REGEX")    f.op = FilterOp::REGEX;
+                        else if (opStr == "SEARCH")   f.op = FilterOp::SEARCH;
+                        else {
+                            spdlog::error("create_view '{}': unknown filter op '{}'", name, opStr);
+                            return false;
+                        }
+                    } else if (opJson.is_number()) {
+                        f.op = static_cast<FilterOp>(opJson.get<int>());
+                    }
+                } else {
+                    f.op = FilterOp::EQ;
+                }
+
+                f.value = fj.value("value", nlohmann::json());
+                v.where.push_back(f);
+            }
+        }
+
+        // Parse default_sort
+        if (operation.contains("default_sort") && operation["default_sort"].is_object()) {
+            Sort s;
+            s.field = operation["default_sort"].value("field", "");
+            s.descending = operation["default_sort"].value("descending", false);
+            if (!s.field.empty()) {
+                v.defaultSort = s;
+            }
+        }
+
+        std::string err;
+        bool ok = view_manager_.createView(v, err);
+        if (!ok) {
+            // Idempotent: "already exists" is not an error when replaying migrations
+            if (err.find("already exists") != std::string::npos) {
+                spdlog::debug("create_view '{}': already exists (idempotent)", name);
+                return true;
+            }
+            spdlog::error("create_view '{}': {}", name, err);
+            return false;
+        }
+        spdlog::info("create_view '{}' over '{}': ok", name, collection);
+        return true;
+    }
+
     if (type == "insert") {
         std::string collection = operation.value("collection", "");
         std::string id = operation.value("id", "");

+ 8 - 1
service/src/migrations/migration_runner.hpp

@@ -1,6 +1,7 @@
 #pragma once
 
 #include "../memory_store.hpp"
+#include "../views/view_manager.hpp"
 
 #include <filesystem>
 #include <string>
@@ -44,6 +45,11 @@ namespace smartbotic::database {
  *   `bytes` bytes (default 32) and upsert it at `{collection}/{key}` as
  *   `{"value": "<hex>"}`. If the key already exists, the secret is
  *   preserved (never rotated by migrations).
+ * - create_view: Declare a view over an existing collection. Supports
+ *   `include`, `exclude`, `where` (with all FilterOp values — EQ/NE/GT/
+ *   GTE/LT/LTE/IN/CONTAINS/EXISTS/REGEX/SEARCH — via string or integer
+ *   op field), and `default_sort` (field + descending). Idempotent:
+ *   "already exists" is treated as success on migration replay.
  */
 class MigrationRunner {
 public:
@@ -53,7 +59,7 @@ public:
         bool failOnError = true;
     };
 
-    explicit MigrationRunner(MemoryStore& store, Config config);
+    MigrationRunner(MemoryStore& store, ViewManager& view_manager, Config config);
 
     /**
      * Run all pending migrations.
@@ -103,6 +109,7 @@ private:
     std::vector<std::filesystem::path> getMigrationFiles() const;
 
     MemoryStore& store_;
+    ViewManager& view_manager_;
     Config config_;
 
     /**