|
@@ -1,2 +1,72 @@
|
|
|
|
|
+#include "collection_registry.hpp"
|
|
|
|
|
+#include "errors.hpp"
|
|
|
|
|
+#include "json_http.hpp"
|
|
|
#include "server.hpp"
|
|
#include "server.hpp"
|
|
|
-namespace svapi { void registerCollectionRoutes(ApiServer&) {} }
|
|
|
|
|
|
|
+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}});
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+}
|