#include "collection_registry.hpp" #include "errors.hpp" #include "filters.hpp" #include "json_http.hpp" #include "server.hpp" 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, KeyOp op) { ApiKey k = requireKey(*d, req); std::string project = req.matches[projIdx], name = req.matches[collIdx]; requireCapability(*d, k, req, project, name, op); 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, KeyOp::Insert); 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, KeyOp::Read); 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, KeyOp::List); 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)); auto parseUInt = [](const std::string& v, const char* name) -> uint32_t { try { return (uint32_t)std::stoul(v); } catch (...) { throw ApiError(ErrCode::Unprocessable, "validation", std::string(name) + " must be a non-negative integer"); } }; if (req.has_param("limit")) opts.limit = parseUInt(req.get_param_value("limit"), "limit"); if (req.has_param("offset")) opts.offset = parseUInt(req.get_param_value("offset"), "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, KeyOp::Update); 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, KeyOp::Update); 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, KeyOp::Delete); 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}}); }); } }