|
@@ -0,0 +1,78 @@
|
|
|
|
|
+#include "errors.hpp"
|
|
|
|
|
+#include "json_http.hpp"
|
|
|
|
|
+#include "server.hpp"
|
|
|
|
|
+
|
|
|
|
|
+namespace svapi {
|
|
|
|
|
+
|
|
|
|
|
+void registerIndexRoutes(ApiServer& s) {
|
|
|
|
|
+ auto& svr = s.raw(); ServerDeps* d = &s.deps();
|
|
|
|
|
+
|
|
|
|
|
+ // Declare an index. Backfills existing rows, so it is usable immediately.
|
|
|
|
|
+ // Idempotent: re-declaring an existing index reports already_existed.
|
|
|
|
|
+ svr.Post(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/indexes)",
|
|
|
|
|
+ [d](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ ApiKey k = requireKey(*d, req);
|
|
|
|
|
+ std::string project = req.matches[1], coll = req.matches[2];
|
|
|
|
|
+ requireProjectManage(k, project);
|
|
|
|
|
+ auto body = bodyJson(req);
|
|
|
|
|
+ // A request body of literal `null` (or any non-object JSON) parses fine but
|
|
|
|
|
+ // is not an object; guard before .value() so it 422s as "missing field"
|
|
|
|
|
+ // rather than 500ing on a type_error.
|
|
|
|
|
+ std::string field = body.is_object() ? body.value("field", "") : std::string();
|
|
|
|
|
+ if (field.empty()) throw ApiError(ErrCode::Unprocessable, "validation", "field is required");
|
|
|
|
|
+
|
|
|
|
|
+ const std::string qc = qualify(project, coll);
|
|
|
|
|
+ if (!d->db.client().getCollectionInfo(qc))
|
|
|
|
|
+ throw ApiError(ErrCode::NotFound, "not_found", "collection not found");
|
|
|
|
|
+
|
|
|
|
|
+ // createIndex() is idempotent on the DB side but does not itself report
|
|
|
|
|
+ // whether the index already existed, so check the current list first.
|
|
|
|
|
+ bool alreadyExisted = false;
|
|
|
|
|
+ {
|
|
|
|
|
+ auto existing = d->db.client().listIndexes(qc);
|
|
|
|
|
+ for (const auto& def : existing) if (def.field == field) { alreadyExisted = true; break; }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ uint64_t rows = 0;
|
|
|
|
|
+ if (!d->db.client().createIndex(qc, field, rows))
|
|
|
|
|
+ throw ApiError(ErrCode::Unavailable, "db_error", "failed to create index");
|
|
|
|
|
+ sendJson(res, 201, {{"field", field}, {"unique", false},
|
|
|
|
|
+ {"rows_indexed", rows}, {"already_existed", alreadyExisted}});
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ svr.Get(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/indexes)",
|
|
|
|
|
+ [d](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ ApiKey k = requireKey(*d, req);
|
|
|
|
|
+ std::string project = req.matches[1], coll = req.matches[2];
|
|
|
|
|
+ requireProjectAccess(k, project);
|
|
|
|
|
+
|
|
|
|
|
+ const std::string qc = qualify(project, coll);
|
|
|
|
|
+ if (!d->db.client().getCollectionInfo(qc))
|
|
|
|
|
+ throw ApiError(ErrCode::NotFound, "not_found", "collection not found");
|
|
|
|
|
+
|
|
|
|
|
+ std::vector<bool> uniqueFlags;
|
|
|
|
|
+ auto defs = d->db.client().listIndexes(qc, uniqueFlags);
|
|
|
|
|
+ nlohmann::json out = nlohmann::json::array();
|
|
|
|
|
+ for (size_t i = 0; i < defs.size(); ++i) {
|
|
|
|
|
+ const auto& def = defs[i];
|
|
|
|
|
+ const bool uniq = i < uniqueFlags.size() && uniqueFlags[i];
|
|
|
|
|
+ out.push_back({{"field", def.field},
|
|
|
|
|
+ {"distinct_values", def.distinctValues},
|
|
|
|
|
+ {"entries", def.entries},
|
|
|
|
|
+ {"unique", uniq}});
|
|
|
|
|
+ }
|
|
|
|
|
+ sendJson(res, 200, {{"indexes", out}});
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ svr.Delete(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/indexes/([^/]+))",
|
|
|
|
|
+ [d](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ ApiKey k = requireKey(*d, req);
|
|
|
|
|
+ std::string project = req.matches[1], coll = req.matches[2], field = req.matches[3];
|
|
|
|
|
+ requireProjectManage(k, project);
|
|
|
|
|
+ if (!d->db.client().dropIndex(qualify(project, coll), field))
|
|
|
|
|
+ throw ApiError(ErrCode::NotFound, "not_found", "index not found");
|
|
|
|
|
+ sendJson(res, 200, {{"dropped", field}});
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+} // namespace svapi
|