documents.cpp 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. #include "collection_registry.hpp"
  2. #include "errors.hpp"
  3. #include "filters.hpp"
  4. #include "json_http.hpp"
  5. #include "server.hpp"
  6. #include <cstdlib>
  7. #include <limits>
  8. #include <optional>
  9. namespace svapi {
  10. namespace {
  11. // Authorize + verify the collection exists; returns the qualified collection name.
  12. std::string scoped(ServerDeps* d, const httplib::Request& req, int projIdx, int collIdx, KeyOp op) {
  13. ApiKey k = requireKey(*d, req);
  14. std::string project = req.matches[projIdx], name = req.matches[collIdx];
  15. requireCapability(*d, k, req, project, name, op);
  16. if (!d->registry.get(project, name)) throw ApiError(ErrCode::NotFound, "not_found", "no such collection");
  17. return qualify(project, name);
  18. }
  19. // Parses ?ttl_seconds=. Returns nullopt when absent — the caller must
  20. // distinguish "not supplied" (leave expiry alone) from 0 (clear the expiry).
  21. std::optional<uint32_t> ttlParam(const httplib::Request& req) {
  22. if (!req.has_param("ttl_seconds")) return std::nullopt;
  23. const std::string raw = req.get_param_value("ttl_seconds");
  24. if (raw.empty() || raw.find_first_not_of("0123456789") != std::string::npos)
  25. throw ApiError(ErrCode::Unprocessable, "validation", "ttl_seconds must be a non-negative integer");
  26. unsigned long long wide = std::strtoull(raw.c_str(), nullptr, 10);
  27. if (wide > std::numeric_limits<uint32_t>::max())
  28. throw ApiError(ErrCode::Unprocessable, "validation", "ttl_seconds must be a non-negative integer");
  29. return (uint32_t)wide;
  30. }
  31. }
  32. void registerDocumentRoutes(ApiServer& s) {
  33. auto& svr = s.raw(); ServerDeps* d = &s.deps();
  34. svr.Post(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/documents)", [d](const httplib::Request& req, httplib::Response& res) {
  35. std::string c = scoped(d, req, 1, 2, KeyOp::Insert);
  36. auto body = bodyJson(req);
  37. std::string id = body.value("id", "");
  38. nlohmann::json data = body.contains("data") ? body["data"] : body;
  39. if (data.is_object() && data.contains("id")) data.erase("id");
  40. const uint32_t ttl = ttlParam(req).value_or(0);
  41. std::string newId = d->db.client().insert(c, data, id, ttl);
  42. if (newId.empty()) throw ApiError(ErrCode::Unavailable, "db_error", "insert failed");
  43. sendJson(res, 201, {{"id", newId}});
  44. });
  45. svr.Get(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/documents/([^/]+))", [d](const httplib::Request& req, httplib::Response& res) {
  46. std::string c = scoped(d, req, 1, 2, KeyOp::Read);
  47. auto doc = d->db.client().get(c, req.matches[3]);
  48. if (!doc) throw ApiError(ErrCode::NotFound, "not_found", "no such document");
  49. sendJson(res, 200, *doc);
  50. });
  51. svr.Get(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/documents)", [d](const httplib::Request& req, httplib::Response& res) {
  52. std::string c = scoped(d, req, 1, 2, KeyOp::List);
  53. smartbotic::database::Client::QueryOptions opts;
  54. auto range = req.params.equal_range("filter");
  55. for (auto it = range.first; it != range.second; ++it) opts.filters.push_back(parseFilter(it->second));
  56. auto parseUInt = [](const std::string& v, const char* name) -> uint32_t {
  57. try { return (uint32_t)std::stoul(v); }
  58. catch (...) { throw ApiError(ErrCode::Unprocessable, "validation", std::string(name) + " must be a non-negative integer"); }
  59. };
  60. if (req.has_param("limit")) opts.limit = parseUInt(req.get_param_value("limit"), "limit");
  61. if (req.has_param("offset")) opts.offset = parseUInt(req.get_param_value("offset"), "offset");
  62. if (req.has_param("sort")) opts.sortField = req.get_param_value("sort");
  63. if (req.has_param("desc")) opts.sortDescending = (req.get_param_value("desc") == "true");
  64. auto docs = d->db.client().find(c, opts);
  65. sendJson(res, 200, {{"documents", docs}, {"count", docs.size()}});
  66. });
  67. svr.Patch(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/documents/([^/]+))", [d](const httplib::Request& req, httplib::Response& res) {
  68. std::string c = scoped(d, req, 1, 2, KeyOp::Update); std::string id = req.matches[3];
  69. if (!d->db.client().exists(c, id)) throw ApiError(ErrCode::NotFound, "not_found", "no such document");
  70. nlohmann::json patchBody = bodyJson(req);
  71. const auto ttl = ttlParam(req);
  72. uint64_t v = ttl ? d->db.client().patchWithTtl(c, id, patchBody, *ttl)
  73. : d->db.client().patch(c, id, patchBody);
  74. if (v == 0) throw ApiError(ErrCode::Unavailable, "db_error", "patch failed");
  75. sendJson(res, 200, {{"id", id}, {"version", v}});
  76. });
  77. svr.Put(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/documents/([^/]+))", [d](const httplib::Request& req, httplib::Response& res) {
  78. std::string c = scoped(d, req, 1, 2, KeyOp::Update); std::string id = req.matches[3];
  79. if (!d->db.client().exists(c, id)) throw ApiError(ErrCode::NotFound, "not_found", "no such document");
  80. if (!d->db.client().update(c, id, bodyJson(req))) throw ApiError(ErrCode::Unavailable, "db_error", "update failed");
  81. sendJson(res, 200, {{"id", id}});
  82. });
  83. svr.Delete(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/documents/([^/]+))", [d](const httplib::Request& req, httplib::Response& res) {
  84. std::string c = scoped(d, req, 1, 2, KeyOp::Delete); std::string id = req.matches[3];
  85. std::string project = req.matches[1];
  86. std::string err;
  87. if (!d->db.client().remove(c, id, err)) {
  88. // A restrict relation is the one refusal a caller can act on, so it
  89. // gets 409 plus the impact list rather than a generic failure.
  90. auto why = d->db.client().describeDelete(c, id);
  91. if (why.success && why.wouldBeBlocked)
  92. throw ApiError(ErrCode::Conflict, "relation_restricted",
  93. err.empty() ? "delete blocked by a relation" : err,
  94. {{"impacts", impactsJson(project, why)}});
  95. throw ApiError(ErrCode::NotFound, "not_found", "no such document");
  96. }
  97. sendJson(res, 200, {{"deleted", id}});
  98. });
  99. }
  100. }