|
|
@@ -0,0 +1,93 @@
|
|
|
+#include "errors.hpp"
|
|
|
+#include "json_http.hpp"
|
|
|
+#include "server.hpp"
|
|
|
+
|
|
|
+namespace svapi {
|
|
|
+namespace {
|
|
|
+
|
|
|
+// Strip a leading "project:" so the API never leaks qualified names outward.
|
|
|
+std::string unqualify(const std::string& project, const std::string& name) {
|
|
|
+ const std::string pfx = project + ":";
|
|
|
+ return name.rfind(pfx, 0) == 0 ? name.substr(pfx.size()) : name;
|
|
|
+}
|
|
|
+
|
|
|
+nlohmann::json relJson(const std::string& project,
|
|
|
+ const smartbotic::database::Client::RelationDefinition& r) {
|
|
|
+ return {{"name", unqualify(project, r.name)},
|
|
|
+ {"child", unqualify(project, r.child)},
|
|
|
+ {"child_field", r.childField},
|
|
|
+ {"parent", unqualify(project, r.parent)},
|
|
|
+ {"on_delete", r.onDelete},
|
|
|
+ {"validate_on_write", r.validateOnWrite},
|
|
|
+ {"created_at", r.createdAt},
|
|
|
+ {"updated_at", r.updatedAt}};
|
|
|
+}
|
|
|
+
|
|
|
+} // namespace
|
|
|
+
|
|
|
+void registerRelationRoutes(ApiServer& s) {
|
|
|
+ auto& svr = s.raw(); ServerDeps* d = &s.deps();
|
|
|
+
|
|
|
+ svr.Post(R"(/api/v1/projects/([^/]+)/relations)",
|
|
|
+ [d](const httplib::Request& req, httplib::Response& res) {
|
|
|
+ ApiKey k = requireKey(*d, req); std::string project = req.matches[1];
|
|
|
+ requireAdmin(k); requireProjectAccess(k, project);
|
|
|
+ auto body = bodyJson(req);
|
|
|
+ std::string name = body.is_object() ? body.value("name", "") : std::string(),
|
|
|
+ child = body.is_object() ? body.value("child", "") : std::string(),
|
|
|
+ field = body.is_object() ? body.value("child_field", "") : std::string(),
|
|
|
+ parent = body.is_object() ? body.value("parent", "") : std::string(),
|
|
|
+ onDelete = body.is_object() ? body.value("on_delete", "restrict") : std::string("restrict");
|
|
|
+ if (name.empty() || child.empty() || field.empty() || parent.empty())
|
|
|
+ throw ApiError(ErrCode::Unprocessable, "validation",
|
|
|
+ "name, child, child_field and parent are 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 (onDelete != "restrict" && onDelete != "cascade" &&
|
|
|
+ onDelete != "set_null" && onDelete != "no_action")
|
|
|
+ throw ApiError(ErrCode::Unprocessable, "validation",
|
|
|
+ "on_delete must be restrict, cascade, set_null or no_action");
|
|
|
+ const bool validateOnWrite = body.is_object() ? body.value("validate_on_write", false) : false;
|
|
|
+
|
|
|
+ // The relation NAME is project-qualified exactly like the collections.
|
|
|
+ if (!d->db.client().createRelation(qualify(project, name), qualify(project, child), field,
|
|
|
+ qualify(project, parent), onDelete, validateOnWrite))
|
|
|
+ throw ApiError(ErrCode::Unavailable, "db_error", "failed to create relation");
|
|
|
+ sendJson(res, 201, {{"name", name}, {"child", child}, {"child_field", field},
|
|
|
+ {"parent", parent}, {"on_delete", onDelete},
|
|
|
+ {"validate_on_write", validateOnWrite}});
|
|
|
+ });
|
|
|
+
|
|
|
+ svr.Get(R"(/api/v1/projects/([^/]+)/relations)",
|
|
|
+ [d](const httplib::Request& req, httplib::Response& res) {
|
|
|
+ ApiKey k = requireKey(*d, req); std::string project = req.matches[1];
|
|
|
+ requireAdmin(k); requireProjectAccess(k, project);
|
|
|
+ auto defs = d->db.client().listRelations(project);
|
|
|
+ nlohmann::json out = nlohmann::json::array();
|
|
|
+ for (const auto& r : defs) out.push_back(relJson(project, r));
|
|
|
+ sendJson(res, 200, {{"relations", out}});
|
|
|
+ });
|
|
|
+
|
|
|
+ svr.Get(R"(/api/v1/projects/([^/]+)/relations/([^/]+))",
|
|
|
+ [d](const httplib::Request& req, httplib::Response& res) {
|
|
|
+ ApiKey k = requireKey(*d, req);
|
|
|
+ std::string project = req.matches[1], name = req.matches[2];
|
|
|
+ requireAdmin(k); requireProjectAccess(k, project);
|
|
|
+ auto info = d->db.client().getRelationInfo(qualify(project, name));
|
|
|
+ if (!info) throw ApiError(ErrCode::NotFound, "not_found", "relation not found");
|
|
|
+ sendJson(res, 200, relJson(project, *info));
|
|
|
+ });
|
|
|
+
|
|
|
+ svr.Delete(R"(/api/v1/projects/([^/]+)/relations/([^/]+))",
|
|
|
+ [d](const httplib::Request& req, httplib::Response& res) {
|
|
|
+ ApiKey k = requireKey(*d, req);
|
|
|
+ std::string project = req.matches[1], name = req.matches[2];
|
|
|
+ requireAdmin(k); requireProjectAccess(k, project);
|
|
|
+ if (!d->db.client().dropRelation(qualify(project, name)))
|
|
|
+ throw ApiError(ErrCode::NotFound, "not_found", "relation not found");
|
|
|
+ sendJson(res, 200, {{"dropped", name}});
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+} // namespace svapi
|