#include "collection_registry.hpp" #include "errors.hpp" #include "json_http.hpp" #include "server.hpp" namespace svapi { void registerCollectionRoutes(ApiServer& s) { auto& svr = s.raw(); ServerDeps* d = &s.deps(); svr.Post(R"(/api/v1/projects/([^/]+)/collections)", [d](const httplib::Request& req, httplib::Response& res) { ApiKey k = requireKey(*d, req); std::string project = req.matches[1]; requireProjectAccess(k, project); d->db.ensureProject(project); d->registry.ensure(project); auto body = bodyJson(req); std::string name = body.value("name", ""), kind = body.value("kind", "json"); if (name.empty()) throw ApiError(ErrCode::Unprocessable, "validation", "name is required"); if (name.rfind('_', 0) == 0 || name.rfind("vectorapi_", 0) == 0) throw ApiError(ErrCode::Unprocessable, "validation", "name may not start with '_' or the reserved 'vectorapi_' prefix"); if (kind != "json" && kind != "vector") throw ApiError(ErrCode::Unprocessable, "validation", "kind must be 'json' or 'vector'"); uint32_t dim = 0; std::string model; if (kind == "vector") { if (!body.contains("vector_dimension")) throw ApiError(ErrCode::Unprocessable, "validation", "vector_dimension required for vector collections"); dim = body.value("vector_dimension", 0u); if (dim == 0) throw ApiError(ErrCode::Unprocessable, "validation", "vector_dimension must be > 0"); model = body.value("embedding_model", ""); } if (d->registry.get(project, name)) throw ApiError(ErrCode::Unprocessable, "exists", "collection already exists"); // Drop any stale DB-layer collection (e.g. left from a prior dropProject that didn't // fully clean up its data — DB v2.3.1 Stage F caveat). if (d->db.client().getCollectionInfo(qualify(project, name))) d->db.client().dropCollection(qualify(project, name)); if (!d->db.client().createCollection(qualify(project, name), 0, false, 0, dim)) throw ApiError(ErrCode::Unavailable, "db_error", "failed to create collection"); d->registry.add(project, {name, kind, dim, model, 0}); sendJson(res, 201, {{"name", name}, {"kind", kind}, {"vector_dimension", dim}}); }); svr.Get(R"(/api/v1/projects/([^/]+)/collections)", [d](const httplib::Request& req, httplib::Response& res) { ApiKey k = requireKey(*d, req); std::string project = req.matches[1]; requireProjectAccess(k, project); nlohmann::json out = nlohmann::json::array(); for (const auto& m : d->registry.list(project)) { nlohmann::json e = m.toJson(); if (auto info = d->db.client().getCollectionInfo(qualify(project, m.name))) { e["document_count"] = info->documentCount; e["size_bytes"] = info->sizeBytes; } out.push_back(e); } sendJson(res, 200, {{"collections", out}}); }); svr.Get(R"(/api/v1/projects/([^/]+)/collections/([^/]+))", [d](const httplib::Request& req, httplib::Response& res) { ApiKey k = requireKey(*d, req); std::string project = req.matches[1], name = req.matches[2]; requireProjectAccess(k, project); auto m = d->registry.get(project, name); if (!m) throw ApiError(ErrCode::NotFound, "not_found", "no such collection"); nlohmann::json e = m->toJson(); if (auto info = d->db.client().getCollectionInfo(qualify(project, name))) { e["document_count"] = info->documentCount; e["size_bytes"] = info->sizeBytes; } sendJson(res, 200, e); }); svr.Delete(R"(/api/v1/projects/([^/]+)/collections/([^/]+))", [d](const httplib::Request& req, httplib::Response& res) { ApiKey k = requireKey(*d, req); std::string project = req.matches[1], name = req.matches[2]; requireProjectAccess(k, project); if (!d->registry.get(project, name)) throw ApiError(ErrCode::NotFound, "not_found", "no such collection"); d->db.client().dropCollection(qualify(project, name)); d->registry.remove(project, name); sendJson(res, 200, {{"deleted", name}}); }); } }