Преглед на файлове

feat(views): client API — createView/dropView/listViews/getViewInfo

Reuses Client::Filter and Client::FilterOp from v1.4.2 for baked-in
where filters. Adds Client::Sort struct and ViewDefinition struct.
Composition semantics documented in the method docstring.
fszontagh преди 3 месеца
родител
ревизия
212cc9b920
променени са 2 файла, в които са добавени 214 реда и са изтрити 0 реда
  1. 61 0
      client/include/smartbotic/database/client.hpp
  2. 153 0
      client/src/client.cpp

+ 61 - 0
client/include/smartbotic/database/client.hpp

@@ -287,6 +287,67 @@ public:
      */
     [[nodiscard]] std::optional<CollectionInfo> getCollectionInfo(const std::string& name);
 
+    // ===== View Management =====
+
+    struct Sort {
+        std::string field;
+        bool descending = false;
+    };
+
+    /**
+     * View definition returned by listViews() and getViewInfo().
+     * Mirrors the server-side ViewDefinition proto message.
+     */
+    struct ViewDefinition {
+        std::string name;
+        std::string collection;
+        std::vector<std::string> include;
+        std::vector<std::string> exclude;
+        std::vector<Filter> where;                // baked-in filters (AND-merged)
+        std::optional<Sort> defaultSort;          // default sort (caller can override)
+        uint64_t createdAt = 0;
+        uint64_t updatedAt = 0;
+    };
+
+    /**
+     * Create a read-only view over a collection.
+     *
+     * View composition semantics:
+     *   - Filters (where):  view AND caller — both applied on queries
+     *   - Sort (defaultSort): caller overrides view — view default used only
+     *                         when caller doesn't specify a sort
+     *   - Include/exclude: enforced server-side on all query responses
+     *
+     * @param name View name (unique, cannot start with '_')
+     * @param collection Target real collection (must not be another view)
+     * @param include Field paths to keep (dot-notation; e.g. "user.profile.name")
+     * @param exclude Field paths to hide (ignored if include is non-empty)
+     * @param where Baked-in filters (always applied on queries through this view)
+     * @param defaultSort Default sort (caller can override per-query)
+     * @return true on success
+     */
+    bool createView(const std::string& name,
+                    const std::string& collection,
+                    const std::vector<std::string>& include = {},
+                    const std::vector<std::string>& exclude = {},
+                    const std::vector<Filter>& where = {},
+                    const std::optional<Sort>& defaultSort = std::nullopt);
+
+    /**
+     * Drop a view by name.
+     */
+    bool dropView(const std::string& name);
+
+    /**
+     * List all views.
+     */
+    [[nodiscard]] std::vector<ViewDefinition> listViews();
+
+    /**
+     * Look up a single view by name.
+     */
+    [[nodiscard]] std::optional<ViewDefinition> getViewInfo(const std::string& name);
+
     // ===== Event Subscription =====
 
     using EventCallback = std::function<void(const std::string& collection,

+ 153 - 0
client/src/client.cpp

@@ -628,6 +628,138 @@ public:
         return result;
     }
 
+    // ===== View Management =====
+
+    bool createView(const std::string& name,
+                    const std::string& collection,
+                    const std::vector<std::string>& include,
+                    const std::vector<std::string>& exclude,
+                    const std::vector<Client::Filter>& where,
+                    const std::optional<Client::Sort>& defaultSort) {
+        smartbotic::databasepb::CreateViewRequest request;
+        request.set_name(name);
+        request.set_collection(collection);
+        for (const auto& p : include) request.add_include(p);
+        for (const auto& p : exclude) request.add_exclude(p);
+
+        for (const auto& f : where) {
+            auto* pb = request.add_where();
+            pb->set_field(f.field);
+            pb->set_op(static_cast<smartbotic::databasepb::FilterOp>(f.op));
+            pb->set_value(f.value.dump());
+        }
+
+        if (defaultSort) {
+            auto* s = request.mutable_default_sort();
+            s->set_field(defaultSort->field);
+            s->set_descending(defaultSort->descending);
+        }
+
+        smartbotic::databasepb::CreateViewResponse response;
+        grpc::ClientContext context;
+        setDeadline(context);
+
+        auto status = stub_->CreateView(&context, request, &response);
+        if (!status.ok()) {
+            spdlog::error("Client::createView failed: {}", status.error_message());
+            return false;
+        }
+        if (!response.success()) {
+            spdlog::error("Client::createView rejected: {}", response.error());
+            return false;
+        }
+        return true;
+    }
+
+    bool dropView(const std::string& name) {
+        smartbotic::databasepb::DropViewRequest request;
+        request.set_name(name);
+        smartbotic::databasepb::DropViewResponse response;
+        grpc::ClientContext context;
+        setDeadline(context);
+
+        auto status = stub_->DropView(&context, request, &response);
+        if (!status.ok()) {
+            spdlog::error("Client::dropView failed: {}", status.error_message());
+            return false;
+        }
+        return response.success();
+    }
+
+    std::vector<Client::ViewDefinition> listViews() {
+        smartbotic::databasepb::ListViewsRequest request;
+        smartbotic::databasepb::ListViewsResponse response;
+        grpc::ClientContext context;
+        setDeadline(context);
+
+        std::vector<Client::ViewDefinition> out;
+        auto status = stub_->ListViews(&context, request, &response);
+        if (!status.ok()) {
+            spdlog::error("Client::listViews failed: {}", status.error_message());
+            return out;
+        }
+        for (const auto& pb : response.views()) {
+            Client::ViewDefinition v;
+            v.name = pb.name();
+            v.collection = pb.collection();
+            for (const auto& p : pb.include()) v.include.push_back(p);
+            for (const auto& p : pb.exclude()) v.exclude.push_back(p);
+            for (const auto& pbf : pb.where()) {
+                Client::Filter f;
+                f.field = pbf.field();
+                f.op = static_cast<Client::FilterOp>(pbf.op());
+                try { f.value = nlohmann::json::parse(pbf.value()); }
+                catch (...) { f.value = pbf.value(); }
+                v.where.push_back(f);
+            }
+            if (pb.has_default_sort()) {
+                Client::Sort s;
+                s.field = pb.default_sort().field();
+                s.descending = pb.default_sort().descending();
+                v.defaultSort = s;
+            }
+            v.createdAt = pb.created_at();
+            v.updatedAt = pb.updated_at();
+            out.push_back(std::move(v));
+        }
+        return out;
+    }
+
+    std::optional<Client::ViewDefinition> getViewInfo(const std::string& name) {
+        smartbotic::databasepb::GetViewInfoRequest request;
+        request.set_name(name);
+        smartbotic::databasepb::GetViewInfoResponse response;
+        grpc::ClientContext context;
+        setDeadline(context);
+
+        auto status = stub_->GetViewInfo(&context, request, &response);
+        if (!status.ok() || !response.found()) {
+            return std::nullopt;
+        }
+        Client::ViewDefinition v;
+        v.name = response.view().name();
+        v.collection = response.view().collection();
+        for (const auto& p : response.view().include()) v.include.push_back(p);
+        for (const auto& p : response.view().exclude()) v.exclude.push_back(p);
+        for (const auto& pbf : response.view().where()) {
+            Client::Filter f;
+            f.field = pbf.field();
+            f.op = static_cast<Client::FilterOp>(pbf.op());
+            try { f.value = nlohmann::json::parse(pbf.value()); }
+            catch (...) { f.value = pbf.value(); }
+            v.where.push_back(f);
+        }
+        if (response.view().has_default_sort()) {
+            Client::Sort s;
+            s.field = response.view().default_sort().field();
+            s.descending = response.view().default_sort().descending();
+            v.defaultSort = s;
+        }
+        v.createdAt = response.view().created_at();
+        v.updatedAt = response.view().updated_at();
+        return v;
+    }
+
     // ===== Event Subscription =====
 
     class SubscriptionHandle {
@@ -1234,6 +1366,27 @@ std::optional<Client::CollectionInfo> Client::getCollectionInfo(const std::strin
     return impl_->getCollectionInfo(name);
 }
 
+bool Client::createView(const std::string& name,
+                        const std::string& collection,
+                        const std::vector<std::string>& include,
+                        const std::vector<std::string>& exclude,
+                        const std::vector<Filter>& where,
+                        const std::optional<Sort>& defaultSort) {
+    return impl_->createView(name, collection, include, exclude, where, defaultSort);
+}
+
+bool Client::dropView(const std::string& name) {
+    return impl_->dropView(name);
+}
+
+std::vector<Client::ViewDefinition> Client::listViews() {
+    return impl_->listViews();
+}
+
+std::optional<Client::ViewDefinition> Client::getViewInfo(const std::string& name) {
+    return impl_->getViewInfo(name);
+}
+
 std::shared_ptr<void> Client::subscribe(const std::vector<std::string>& collections, EventCallback callback) {
     return impl_->subscribe(collections, std::move(callback));
 }