|
@@ -1,4 +1,5 @@
|
|
|
#include "project_controller.hpp"
|
|
#include "project_controller.hpp"
|
|
|
|
|
+#include "storage/retention.hpp"
|
|
|
|
|
|
|
|
#include "common/time_utils.hpp"
|
|
#include "common/time_utils.hpp"
|
|
|
#include "common/uuid.hpp"
|
|
#include "common/uuid.hpp"
|
|
@@ -27,8 +28,10 @@ const std::vector<std::string>& ownedCollections() {
|
|
|
ProjectController::ProjectController(storage::StorageClient& storage,
|
|
ProjectController::ProjectController(storage::StorageClient& storage,
|
|
|
auth::AuthMiddleware& middleware,
|
|
auth::AuthMiddleware& middleware,
|
|
|
auth::AccessControl& access,
|
|
auth::AccessControl& access,
|
|
|
- auth::AuthStore& auth_store)
|
|
|
|
|
- : storage_(storage), middleware_(middleware), access_(access), auth_store_(auth_store) {}
|
|
|
|
|
|
|
+ auth::AuthStore& auth_store,
|
|
|
|
|
+ retention::RetentionService& retention)
|
|
|
|
|
+ : storage_(storage), middleware_(middleware), access_(access), auth_store_(auth_store),
|
|
|
|
|
+ retention_(retention) {}
|
|
|
|
|
|
|
|
void ProjectController::registerRoutes(httplib::Server& server) {
|
|
void ProjectController::registerRoutes(httplib::Server& server) {
|
|
|
server.Get("/api/v1/projects", [this](const httplib::Request& req, httplib::Response& res) {
|
|
server.Get("/api/v1/projects", [this](const httplib::Request& req, httplib::Response& res) {
|
|
@@ -227,8 +230,40 @@ void ProjectController::updateProject(const httplib::Request& req, httplib::Resp
|
|
|
patch["name"] = body["name"];
|
|
patch["name"] = body["name"];
|
|
|
}
|
|
}
|
|
|
if (body.contains("description")) patch["description"] = body["description"];
|
|
if (body.contains("description")) patch["description"] = body["description"];
|
|
|
|
|
+
|
|
|
|
|
+ // How long the project's workflows keep their data, unless a workflow says
|
|
|
|
|
+ // otherwise for itself. Validated here rather than trusted: a retention
|
|
|
|
|
+ // stored as a string or a negative number would read back as "nothing
|
|
|
|
|
+ // declared" and silently mean "keep for ever".
|
|
|
|
|
+ const auto before_retention =
|
|
|
|
|
+ storage::declaredTtlSeconds(existing.value().value("settings", nlohmann::json::object()));
|
|
|
|
|
+ std::optional<int64_t> after_retention = before_retention;
|
|
|
|
|
+
|
|
|
|
|
+ if (body.contains("settings")) {
|
|
|
|
|
+ if (!body["settings"].is_object()) {
|
|
|
|
|
+ sendError(res, "settings must be an object", 400);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ nlohmann::json settings = existing.value().value("settings", nlohmann::json::object());
|
|
|
|
|
+ for (const auto& [key, value] : body["settings"].items()) {
|
|
|
|
|
+ settings[key] = value;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (settings.contains(storage::kRetentionKey)) {
|
|
|
|
|
+ const auto& retention = settings[storage::kRetentionKey];
|
|
|
|
|
+ if (!retention.is_object() || !retention.contains(storage::kTtlSecondsKey) ||
|
|
|
|
|
+ !retention[storage::kTtlSecondsKey].is_number_integer() ||
|
|
|
|
|
+ retention[storage::kTtlSecondsKey].get<int64_t>() < 0) {
|
|
|
|
|
+ sendError(res, "retention.ttlSeconds must be a whole number of seconds, "
|
|
|
|
|
+ "0 or more. 0 means keep for ever", 400);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ after_retention = retention[storage::kTtlSecondsKey].get<int64_t>();
|
|
|
|
|
+ }
|
|
|
|
|
+ patch["settings"] = settings;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (patch.empty()) {
|
|
if (patch.empty()) {
|
|
|
- sendError(res, "Nothing to change - send a name or a description", 400);
|
|
|
|
|
|
|
+ sendError(res, "Nothing to change - send a name, a description or settings", 400);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -238,6 +273,29 @@ void ProjectController::updateProject(const httplib::Request& req, httplib::Resp
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // A project's retention is inherited, so changing it has to reach the
|
|
|
|
|
+ // workflows that inherit it - and only those. A workflow that set its own
|
|
|
|
|
+ // has decided for itself, and having a project setting overwrite that would
|
|
|
|
|
+ // make the override meaningless.
|
|
|
|
|
+ if (after_retention != before_retention && after_retention) {
|
|
|
|
|
+ int64_t cascaded = 0;
|
|
|
|
|
+ storage::QueryOptions opts;
|
|
|
|
|
+ opts.filters.emplace_back("projectId", id);
|
|
|
|
|
+ opts.page_size = 500;
|
|
|
|
|
+ auto workflows = storage_.query("workflows", opts);
|
|
|
|
|
+ if (workflows.ok()) {
|
|
|
|
|
+ for (const auto& workflow : workflows.value().documents) {
|
|
|
|
|
+ const auto own = storage::declaredTtlSeconds(
|
|
|
|
|
+ workflow.value("settings", nlohmann::json::object()));
|
|
|
|
|
+ if (own) continue; // it decided for itself
|
|
|
|
|
+ retention_.apply(workflow.value("_id", ""), *after_retention);
|
|
|
|
|
+ cascaded++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ LOG_INFO("Retention: project {} set to {}s - {} workflows inherit it",
|
|
|
|
|
+ id, *after_retention, cascaded);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
auto after = storage_.get(PROJECTS, id);
|
|
auto after = storage_.get(PROJECTS, id);
|
|
|
sendJson(res, describe(after.ok() ? after.value() : existing.value(), ctx));
|
|
sendJson(res, describe(after.ok() ? after.value() : existing.value(), ctx));
|
|
|
}
|
|
}
|