Explorar o código

feat(api): project-scoped JSON document CRUD + find

Fszontagh hai 2 meses
pai
achega
8e2f564736
Modificáronse 2 ficheiros con 90 adicións e 1 borrados
  1. 71 1
      src/handlers/documents.cpp
  2. 19 0
      tests/test_api_integration.cpp

+ 71 - 1
src/handlers/documents.cpp

@@ -1,2 +1,72 @@
+#include "collection_registry.hpp"
+#include "errors.hpp"
+#include "filters.hpp"
+#include "json_http.hpp"
 #include "server.hpp"
-namespace svapi { void registerDocumentRoutes(ApiServer&) {} }
+namespace svapi {
+namespace {
+// Authorize + verify the collection exists; returns the qualified collection name.
+std::string scoped(ServerDeps* d, const httplib::Request& req, int projIdx, int collIdx) {
+    ApiKey k = requireKey(*d, req);
+    std::string project = req.matches[projIdx], name = req.matches[collIdx];
+    requireProjectAccess(k, project);
+    if (!d->registry.get(project, name)) throw ApiError(ErrCode::NotFound, "not_found", "no such collection");
+    return qualify(project, name);
+}
+}
+void registerDocumentRoutes(ApiServer& s) {
+    auto& svr = s.raw(); ServerDeps* d = &s.deps();
+
+    svr.Post(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/documents)", [d](const httplib::Request& req, httplib::Response& res) {
+        std::string c = scoped(d, req, 1, 2);
+        auto body = bodyJson(req);
+        std::string id = body.value("id", "");
+        nlohmann::json data = body.contains("data") ? body["data"] : body;
+        if (data.is_object() && data.contains("id")) data.erase("id");
+        std::string newId = d->db.client().insert(c, data, id);
+        if (newId.empty()) throw ApiError(ErrCode::Unavailable, "db_error", "insert failed");
+        sendJson(res, 201, {{"id", newId}});
+    });
+
+    svr.Get(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/documents/([^/]+))", [d](const httplib::Request& req, httplib::Response& res) {
+        std::string c = scoped(d, req, 1, 2);
+        auto doc = d->db.client().get(c, req.matches[3]);
+        if (!doc) throw ApiError(ErrCode::NotFound, "not_found", "no such document");
+        sendJson(res, 200, *doc);
+    });
+
+    svr.Get(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/documents)", [d](const httplib::Request& req, httplib::Response& res) {
+        std::string c = scoped(d, req, 1, 2);
+        smartbotic::database::Client::QueryOptions opts;
+        auto range = req.params.equal_range("filter");
+        for (auto it = range.first; it != range.second; ++it) opts.filters.push_back(parseFilter(it->second));
+        if (req.has_param("limit"))  opts.limit  = (uint32_t)std::stoul(req.get_param_value("limit"));
+        if (req.has_param("offset")) opts.offset = (uint32_t)std::stoul(req.get_param_value("offset"));
+        if (req.has_param("sort"))   opts.sortField = req.get_param_value("sort");
+        if (req.has_param("desc"))   opts.sortDescending = (req.get_param_value("desc") == "true");
+        auto docs = d->db.client().find(c, opts);
+        sendJson(res, 200, {{"documents", docs}, {"count", docs.size()}});
+    });
+
+    svr.Patch(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/documents/([^/]+))", [d](const httplib::Request& req, httplib::Response& res) {
+        std::string c = scoped(d, req, 1, 2); std::string id = req.matches[3];
+        if (!d->db.client().exists(c, id)) throw ApiError(ErrCode::NotFound, "not_found", "no such document");
+        uint64_t v = d->db.client().patch(c, id, bodyJson(req));
+        if (v == 0) throw ApiError(ErrCode::Unavailable, "db_error", "patch failed");
+        sendJson(res, 200, {{"id", id}, {"version", v}});
+    });
+
+    svr.Put(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/documents/([^/]+))", [d](const httplib::Request& req, httplib::Response& res) {
+        std::string c = scoped(d, req, 1, 2); std::string id = req.matches[3];
+        if (!d->db.client().exists(c, id)) throw ApiError(ErrCode::NotFound, "not_found", "no such document");
+        if (!d->db.client().update(c, id, bodyJson(req))) throw ApiError(ErrCode::Unavailable, "db_error", "update failed");
+        sendJson(res, 200, {{"id", id}});
+    });
+
+    svr.Delete(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/documents/([^/]+))", [d](const httplib::Request& req, httplib::Response& res) {
+        std::string c = scoped(d, req, 1, 2); std::string id = req.matches[3];
+        if (!d->db.client().remove(c, id)) throw ApiError(ErrCode::NotFound, "not_found", "no such document");
+        sendJson(res, 200, {{"deleted", id}});
+    });
+}
+}

+ 19 - 0
tests/test_api_integration.cpp

@@ -97,3 +97,22 @@ TEST_F(ApiFixture, VectorCollectionNeedsDimension) {
     auto bad = c.Post(base.c_str(), nlohmann::json{{"name","novec"},{"kind","vector"}}.dump(), "application/json");
     ASSERT_TRUE(bad); EXPECT_EQ(bad->status, 422);
 }
+
+TEST_F(ApiFixture, DocumentCrudAndFind) {
+    auto c = admin();
+    std::string base = "/api/v1/projects/" + project_ + "/collections";
+    ASSERT_EQ(c.Post(base.c_str(), nlohmann::json{{"name","docs"},{"kind","json"}}.dump(),"application/json")->status, 201);
+    std::string docs = base + "/docs/documents";
+
+    auto ins = c.Post(docs.c_str(), nlohmann::json{{"data",{{"name","Alice"},{"age",30}}}}.dump(), "application/json");
+    ASSERT_EQ(ins->status, 201);
+    std::string id = nlohmann::json::parse(ins->body)["id"];
+    EXPECT_EQ(c.Get((docs + "/" + id).c_str())->status, 200);
+    EXPECT_EQ(c.Patch((docs + "/" + id).c_str(), nlohmann::json{{"age",31}}.dump(), "application/json")->status, 200);
+    auto found = c.Get((docs + "?filter=age:gte:31&limit=10").c_str());
+    ASSERT_EQ(found->status, 200);
+    auto foundJson = nlohmann::json::parse(found->body);
+    EXPECT_GE(foundJson["count"].get<int>(), 1);
+    EXPECT_EQ(c.Delete((docs + "/" + id).c_str())->status, 200);
+    EXPECT_EQ(c.Get((docs + "/" + id).c_str())->status, 404);
+}