22#ifdef PACS_WITH_DATABASE_SYSTEM
31 : base_repository(std::
move(db),
"routing_rules",
"rule_id") {}
40[[nodiscard]] std::string escape_json_string(
const std::string& str) {
41 std::ostringstream oss;
44 case '"': oss <<
"\\\"";
break;
45 case '\\': oss <<
"\\\\";
break;
46 case '\b': oss <<
"\\b";
break;
47 case '\f': oss <<
"\\f";
break;
48 case '\n': oss <<
"\\n";
break;
49 case '\r': oss <<
"\\r";
break;
50 case '\t': oss <<
"\\t";
break;
51 default: oss << c;
break;
58[[nodiscard]] std::string unescape_json_string(std::string_view str) {
60 result.reserve(str.size());
61 for (
size_t i = 0; i < str.size(); ++i) {
62 if (str[i] ==
'\\' && i + 1 < str.size()) {
65 case '"': result +=
'"';
break;
66 case '\\': result +=
'\\';
break;
67 case 'b': result +=
'\b';
break;
68 case 'f': result +=
'\f';
break;
69 case 'n': result +=
'\n';
break;
70 case 'r': result +=
'\r';
break;
71 case 't': result +=
'\t';
break;
72 default: result += str[i];
break;
82[[nodiscard]] std::pair<std::string, size_t> extract_json_string(
83 std::string_view json,
size_t pos) {
84 auto start = json.find(
'"', pos);
85 if (start == std::string_view::npos)
return {
"", std::string_view::npos};
87 size_t end = start + 1;
88 while (end < json.size()) {
89 if (json[end] ==
'\\' && end + 1 < json.size()) {
91 }
else if (json[end] ==
'"') {
98 if (end >= json.size())
return {
"", std::string_view::npos};
100 auto value = unescape_json_string(json.substr(start + 1, end - start - 1));
101 return {value, end + 1};
106std::string routing_repository::serialize_conditions(
107 const std::vector<client::routing_condition>& conditions) {
108 if (conditions.empty())
return "[]";
110 std::ostringstream oss;
112 for (
size_t i = 0; i < conditions.size(); ++i) {
113 if (i > 0) oss <<
",";
114 const auto& cond = conditions[i];
116 oss <<
"\"field\":\"" << client::to_string(cond.match_field) <<
"\",";
117 oss <<
"\"pattern\":\"" << escape_json_string(cond.pattern) <<
"\",";
118 oss <<
"\"case_sensitive\":" << (cond.case_sensitive ?
"true" :
"false") <<
",";
119 oss <<
"\"negate\":" << (cond.negate ?
"true" :
"false");
126std::vector<client::routing_condition> routing_repository::deserialize_conditions(
127 std::string_view json) {
128 std::vector<client::routing_condition> result;
129 if (json.empty() || json ==
"[]")
return result;
132 while (pos < json.size()) {
133 auto obj_start = json.find(
'{', pos);
134 if (obj_start == std::string_view::npos)
break;
136 auto obj_end = json.find(
'}', obj_start);
137 if (obj_end == std::string_view::npos)
break;
139 auto obj = json.substr(obj_start, obj_end - obj_start + 1);
141 client::routing_condition cond;
144 auto field_pos = obj.find(
"\"field\"");
145 if (field_pos != std::string_view::npos) {
146 auto [field_value, next] = extract_json_string(obj, field_pos + 7);
147 cond.match_field = client::routing_field_from_string(field_value);
151 auto pattern_pos = obj.find(
"\"pattern\"");
152 if (pattern_pos != std::string_view::npos) {
153 auto [pattern_value, next] = extract_json_string(obj, pattern_pos + 9);
154 cond.pattern = pattern_value;
158 auto case_pos = obj.find(
"\"case_sensitive\"");
159 if (case_pos != std::string_view::npos) {
160 cond.case_sensitive = (obj.find(
"true", case_pos) != std::string_view::npos &&
161 obj.find(
"true", case_pos) < obj.find(
',', case_pos));
165 auto negate_pos = obj.find(
"\"negate\"");
166 if (negate_pos != std::string_view::npos) {
167 cond.negate = (obj.find(
"true", negate_pos) != std::string_view::npos);
170 result.push_back(std::move(cond));
177std::string routing_repository::serialize_actions(
178 const std::vector<client::routing_action>& actions) {
179 if (actions.empty())
return "[]";
181 std::ostringstream oss;
183 for (
size_t i = 0; i < actions.size(); ++i) {
184 if (i > 0) oss <<
",";
185 const auto& action = actions[i];
187 oss <<
"\"destination\":\"" << escape_json_string(action.destination_node_id) <<
"\",";
188 oss <<
"\"priority\":\"" << client::to_string(action.priority) <<
"\",";
189 oss <<
"\"delay_minutes\":" << action.delay.count() <<
",";
190 oss <<
"\"delete_after_send\":" << (action.delete_after_send ?
"true" :
"false") <<
",";
191 oss <<
"\"notify_on_failure\":" << (action.notify_on_failure ?
"true" :
"false");
198std::vector<client::routing_action> routing_repository::deserialize_actions(
199 std::string_view json) {
200 std::vector<client::routing_action> result;
201 if (json.empty() || json ==
"[]")
return result;
204 while (pos < json.size()) {
205 auto obj_start = json.find(
'{', pos);
206 if (obj_start == std::string_view::npos)
break;
208 auto obj_end = json.find(
'}', obj_start);
209 if (obj_end == std::string_view::npos)
break;
211 auto obj = json.substr(obj_start, obj_end - obj_start + 1);
213 client::routing_action action;
216 auto dest_pos = obj.find(
"\"destination\"");
217 if (dest_pos != std::string_view::npos) {
218 auto [dest_value, next] = extract_json_string(obj, dest_pos + 13);
219 action.destination_node_id = dest_value;
223 auto prio_pos = obj.find(
"\"priority\"");
224 if (prio_pos != std::string_view::npos) {
225 auto [prio_value, next] = extract_json_string(obj, prio_pos + 10);
226 action.priority = client::job_priority_from_string(prio_value);
230 auto delay_pos = obj.find(
"\"delay_minutes\"");
231 if (delay_pos != std::string_view::npos) {
232 auto colon = obj.find(
':', delay_pos);
233 if (colon != std::string_view::npos) {
235 std::sscanf(obj.data() + colon + 1,
"%d", &minutes);
236 action.delay = std::chrono::minutes{minutes};
241 auto delete_pos = obj.find(
"\"delete_after_send\"");
242 if (delete_pos != std::string_view::npos) {
243 action.delete_after_send = (obj.find(
"true", delete_pos) != std::string_view::npos &&
244 obj.find(
"true", delete_pos) < obj.find(
',', delete_pos + 20));
248 auto notify_pos = obj.find(
"\"notify_on_failure\"");
249 if (notify_pos != std::string_view::npos) {
250 action.notify_on_failure = (obj.find(
"true", notify_pos) != std::string_view::npos);
253 result.push_back(std::move(action));
264auto routing_repository::parse_timestamp(
const std::string& str)
const
265 -> std::chrono::system_clock::time_point {
271 std::istringstream ss(str);
272 ss >> std::get_time(&tm,
"%Y-%m-%d %H:%M:%S");
278 auto time = _mkgmtime(&tm);
280 auto time = timegm(&tm);
283 return std::chrono::system_clock::from_time_t(time);
286auto routing_repository::format_timestamp(
287 std::chrono::system_clock::time_point tp)
const -> std::string {
288 if (tp == std::chrono::system_clock::time_point{}) {
292 auto time = std::chrono::system_clock::to_time_t(tp);
295 gmtime_s(&tm, &time);
297 gmtime_r(&time, &tm);
301 std::strftime(buf,
sizeof(buf),
"%Y-%m-%d %H:%M:%S", &tm);
305auto routing_repository::format_optional_timestamp(
306 const std::optional<std::chrono::system_clock::time_point>& tp)
const
308 if (!tp.has_value()) {
311 return format_timestamp(tp.value());
318auto routing_repository::find_by_pk(int64_t pk) -> result_type {
319 if (!db() || !db()->is_connected()) {
320 return result_type(kcenon::common::error_info{
321 -1,
"Database not connected",
"storage"});
324 auto builder = query_builder();
325 builder.select(select_columns())
327 .where(
"pk",
"=", pk)
330 auto result = storage_session().select(builder.build());
331 if (result.is_err()) {
332 return result_type(result.error());
335 if (result.value().empty()) {
336 return result_type(kcenon::common::error_info{
337 -1,
"Rule not found with pk=" + std::to_string(pk),
"storage"});
340 return result_type(map_row_to_entity(result.value()[0]));
343auto routing_repository::find_rules(
const routing_rule_query_options& options)
344 -> list_result_type {
345 if (!db() || !db()->is_connected()) {
346 return list_result_type(kcenon::common::error_info{
347 -1,
"Database not connected",
"storage"});
350 auto builder = query_builder();
351 builder.select(select_columns()).from(table_name());
353 if (options.enabled_only.has_value()) {
354 builder.where(
"enabled",
"=", options.enabled_only.value() ? 1 : 0);
357 if (options.order_by_priority) {
358 builder.order_by(
"priority", database::sort_order::desc);
359 builder.order_by(
"created_at", database::sort_order::asc);
361 builder.order_by(
"created_at", database::sort_order::desc);
364 builder.limit(options.limit).offset(options.offset);
366 auto result = storage_session().select(builder.build());
367 if (result.is_err()) {
368 return list_result_type(result.error());
371 std::vector<client::routing_rule> rules;
372 rules.reserve(result.value().size());
373 for (
const auto& row : result.value()) {
374 rules.push_back(map_row_to_entity(row));
377 return list_result_type(std::move(rules));
380auto routing_repository::find_enabled_rules() -> list_result_type {
381 routing_rule_query_options options;
382 options.enabled_only =
true;
383 options.order_by_priority =
true;
384 return find_rules(options);
391auto routing_repository::update_priority(std::string_view rule_id,
int priority)
393 if (!db() || !db()->is_connected()) {
394 return VoidResult(kcenon::common::error_info{
395 -1,
"Database not connected",
"storage"});
398 auto builder = query_builder();
399 builder.update(table_name())
400 .set(
"priority",
static_cast<int64_t
>(priority))
401 .where(
"rule_id",
"=", std::string(rule_id));
403 auto result = storage_session().execute(builder.build());
404 if (result.is_err()) {
405 return VoidResult(result.error());
408 return kcenon::common::ok();
411auto routing_repository::enable_rule(std::string_view rule_id) -> VoidResult {
412 if (!db() || !db()->is_connected()) {
413 return VoidResult(kcenon::common::error_info{
414 -1,
"Database not connected",
"storage"});
417 auto builder = query_builder();
418 builder.update(table_name())
419 .set(
"enabled",
static_cast<int64_t
>(1))
420 .where(
"rule_id",
"=", std::string(rule_id));
422 auto result = storage_session().execute(builder.build());
423 if (result.is_err()) {
424 return VoidResult(result.error());
427 return kcenon::common::ok();
430auto routing_repository::disable_rule(std::string_view rule_id) -> VoidResult {
431 if (!db() || !db()->is_connected()) {
432 return VoidResult(kcenon::common::error_info{
433 -1,
"Database not connected",
"storage"});
436 auto builder = query_builder();
437 builder.update(table_name())
438 .set(
"enabled",
static_cast<int64_t
>(0))
439 .where(
"rule_id",
"=", std::string(rule_id));
441 auto result = storage_session().execute(builder.build());
442 if (result.is_err()) {
443 return VoidResult(result.error());
446 return kcenon::common::ok();
453auto routing_repository::increment_triggered(std::string_view rule_id)
455 if (!db() || !db()->is_connected()) {
456 return VoidResult(kcenon::common::error_info{
457 -1,
"Database not connected",
"storage"});
460 auto sql =
"UPDATE " + table_name() +
461 " SET triggered_count = triggered_count + 1, "
462 "last_triggered = CURRENT_TIMESTAMP "
463 "WHERE rule_id = '" + std::string(rule_id) +
"'";
465 auto result = storage_session().execute(sql);
466 if (result.is_err()) {
467 return VoidResult(result.error());
470 return kcenon::common::ok();
473auto routing_repository::increment_success(std::string_view rule_id)
475 if (!db() || !db()->is_connected()) {
476 return VoidResult(kcenon::common::error_info{
477 -1,
"Database not connected",
"storage"});
480 auto sql =
"UPDATE " + table_name() +
481 " SET success_count = success_count + 1 WHERE rule_id = '" +
482 std::string(rule_id) +
"'";
484 auto result = storage_session().execute(sql);
485 if (result.is_err()) {
486 return VoidResult(result.error());
489 return kcenon::common::ok();
492auto routing_repository::increment_failure(std::string_view rule_id)
494 if (!db() || !db()->is_connected()) {
495 return VoidResult(kcenon::common::error_info{
496 -1,
"Database not connected",
"storage"});
499 auto sql =
"UPDATE " + table_name() +
500 " SET failure_count = failure_count + 1 WHERE rule_id = '" +
501 std::string(rule_id) +
"'";
503 auto result = storage_session().execute(sql);
504 if (result.is_err()) {
505 return VoidResult(result.error());
508 return kcenon::common::ok();
511auto routing_repository::reset_statistics(std::string_view rule_id)
513 if (!db() || !db()->is_connected()) {
514 return VoidResult(kcenon::common::error_info{
515 -1,
"Database not connected",
"storage"});
518 auto builder = query_builder();
519 builder.update(table_name())
520 .set(
"triggered_count",
static_cast<int64_t
>(0))
521 .set(
"success_count",
static_cast<int64_t
>(0))
522 .set(
"failure_count",
static_cast<int64_t
>(0))
523 .set(
"last_triggered", std::string{})
524 .where(
"rule_id",
"=", std::string(rule_id));
526 auto result = storage_session().execute(builder.build());
527 if (result.is_err()) {
528 return VoidResult(result.error());
531 return kcenon::common::ok();
534auto routing_repository::count_enabled() -> Result<size_t> {
535 if (!db() || !db()->is_connected()) {
536 return Result<size_t>(kcenon::common::error_info{
537 -1,
"Database not connected",
"storage"});
540 auto builder = query_builder();
541 builder.select({
"COUNT(*)"})
543 .where(
"enabled",
"=", 1);
545 auto result = storage_session().select(builder.build());
546 if (result.is_err()) {
547 return Result<size_t>(result.error());
550 if (result.value().empty() || result.value()[0].empty()) {
551 return Result<size_t>(
static_cast<size_t>(0));
554 const auto& row = result.value()[0];
555 auto it = row.find(
"COUNT(*)");
556 if (it == row.end()) {
560 if (it != row.end() && !it->second.empty()) {
561 return Result<size_t>(
static_cast<size_t>(std::stoull(it->second)));
564 return Result<size_t>(
static_cast<size_t>(0));
571auto routing_repository::map_row_to_entity(
const database_row& row)
const
572 -> client::routing_rule {
573 client::routing_rule rule;
576 auto pk_it = row.find(
"pk");
577 if (pk_it != row.end() && !pk_it->second.empty()) {
578 rule.pk = std::stoll(pk_it->second);
581 rule.rule_id = row.at(
"rule_id");
582 rule.name = row.at(
"name");
584 auto desc_it = row.find(
"description");
585 if (desc_it != row.end()) {
586 rule.description = desc_it->second;
589 auto enabled_it = row.find(
"enabled");
590 if (enabled_it != row.end() && !enabled_it->second.empty()) {
591 rule.enabled = (std::stoi(enabled_it->second) != 0);
594 auto priority_it = row.find(
"priority");
595 if (priority_it != row.end() && !priority_it->second.empty()) {
596 rule.priority = std::stoi(priority_it->second);
599 auto conditions_it = row.find(
"conditions_json");
600 if (conditions_it != row.end() && !conditions_it->second.empty()) {
601 rule.conditions = deserialize_conditions(conditions_it->second);
604 auto actions_it = row.find(
"actions_json");
605 if (actions_it != row.end() && !actions_it->second.empty()) {
606 rule.actions = deserialize_actions(actions_it->second);
609 auto schedule_it = row.find(
"schedule_cron");
610 if (schedule_it != row.end() && !schedule_it->second.empty()) {
611 rule.schedule_cron = schedule_it->second;
614 auto eff_from_it = row.find(
"effective_from");
615 if (eff_from_it != row.end() && !eff_from_it->second.empty()) {
616 auto tp = parse_timestamp(eff_from_it->second);
617 if (tp != std::chrono::system_clock::time_point{}) {
618 rule.effective_from = tp;
622 auto eff_until_it = row.find(
"effective_until");
623 if (eff_until_it != row.end() && !eff_until_it->second.empty()) {
624 auto tp = parse_timestamp(eff_until_it->second);
625 if (tp != std::chrono::system_clock::time_point{}) {
626 rule.effective_until = tp;
630 auto triggered_it = row.find(
"triggered_count");
631 if (triggered_it != row.end() && !triggered_it->second.empty()) {
632 rule.triggered_count =
static_cast<size_t>(std::stoll(triggered_it->second));
635 auto success_it = row.find(
"success_count");
636 if (success_it != row.end() && !success_it->second.empty()) {
637 rule.success_count =
static_cast<size_t>(std::stoll(success_it->second));
640 auto failure_it = row.find(
"failure_count");
641 if (failure_it != row.end() && !failure_it->second.empty()) {
642 rule.failure_count =
static_cast<size_t>(std::stoll(failure_it->second));
645 auto last_triggered_it = row.find(
"last_triggered");
646 if (last_triggered_it != row.end() && !last_triggered_it->second.empty()) {
647 rule.last_triggered = parse_timestamp(last_triggered_it->second);
650 auto created_it = row.find(
"created_at");
651 if (created_it != row.end() && !created_it->second.empty()) {
652 rule.created_at = parse_timestamp(created_it->second);
655 auto updated_it = row.find(
"updated_at");
656 if (updated_it != row.end() && !updated_it->second.empty()) {
657 rule.updated_at = parse_timestamp(updated_it->second);
663auto routing_repository::entity_to_row(
const client::routing_rule& entity)
const
664 -> std::map<std::string, database_value> {
665 std::map<std::string, database_value> row;
667 row[
"rule_id"] = entity.rule_id;
668 row[
"name"] = entity.name;
669 row[
"description"] = entity.description;
670 row[
"enabled"] =
static_cast<int64_t
>(entity.enabled ? 1 : 0);
671 row[
"priority"] =
static_cast<int64_t
>(entity.priority);
672 row[
"conditions_json"] = serialize_conditions(entity.conditions);
673 row[
"actions_json"] = serialize_actions(entity.actions);
674 row[
"schedule_cron"] = entity.schedule_cron.value_or(
"");
675 row[
"effective_from"] = format_optional_timestamp(entity.effective_from);
676 row[
"effective_until"] = format_optional_timestamp(entity.effective_until);
677 row[
"triggered_count"] =
static_cast<int64_t
>(entity.triggered_count);
678 row[
"success_count"] =
static_cast<int64_t
>(entity.success_count);
679 row[
"failure_count"] =
static_cast<int64_t
>(entity.failure_count);
680 row[
"last_triggered"] = format_timestamp(entity.last_triggered);
681 row[
"created_at"] = format_timestamp(entity.created_at);
682 row[
"updated_at"] = format_timestamp(entity.updated_at);
687auto routing_repository::get_pk(
const client::routing_rule& entity)
const
689 return entity.rule_id;
692auto routing_repository::has_pk(
const client::routing_rule& entity)
const
694 return !entity.rule_id.empty();
697auto routing_repository::select_columns() const -> std::vector<std::
string> {
699 "pk",
"rule_id",
"name",
"description",
"enabled",
"priority",
700 "conditions_json",
"actions_json",
"schedule_cron",
701 "effective_from",
"effective_until",
702 "triggered_count",
"success_count",
"failure_count",
703 "last_triggered",
"created_at",
"updated_at"
726[[nodiscard]] std::string to_timestamp_string(
727 std::chrono::system_clock::time_point tp) {
728 if (tp == std::chrono::system_clock::time_point{}) {
731 auto time = std::chrono::system_clock::to_time_t(tp);
734 gmtime_s(&tm, &time);
736 gmtime_r(&time, &tm);
739 std::strftime(buf,
sizeof(buf),
"%Y-%m-%d %H:%M:%S", &tm);
744[[nodiscard]] std::chrono::system_clock::time_point from_timestamp_string(
746 if (!str || str[0] ==
'\0') {
750 std::istringstream ss(str);
751 ss >> std::get_time(&tm,
"%Y-%m-%d %H:%M:%S");
756 auto time = _mkgmtime(&tm);
758 auto time = timegm(&tm);
760 return std::chrono::system_clock::from_time_t(time);
764[[nodiscard]] std::optional<std::chrono::system_clock::time_point>
765from_optional_timestamp(
const char* str) {
766 if (!str || str[0] ==
'\0') {
769 auto tp = from_timestamp_string(str);
770 if (tp == std::chrono::system_clock::time_point{}) {
777[[nodiscard]] std::string get_text_column(sqlite3_stmt* stmt,
int col) {
778 auto text =
reinterpret_cast<const char*
>(sqlite3_column_text(stmt, col));
783[[nodiscard]]
int get_int_column(sqlite3_stmt* stmt,
int col,
int default_val = 0) {
784 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
787 return sqlite3_column_int(stmt, col);
791[[nodiscard]] int64_t get_int64_column(sqlite3_stmt* stmt,
int col, int64_t default_val = 0) {
792 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
795 return sqlite3_column_int64(stmt, col);
799[[nodiscard]] std::optional<std::string> get_optional_text(sqlite3_stmt* stmt,
int col) {
800 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
803 auto text =
reinterpret_cast<const char*
>(sqlite3_column_text(stmt, col));
804 return text ? std::optional<std::string>{
text} : std::nullopt;
808void bind_optional_text(sqlite3_stmt* stmt,
int idx,
const std::optional<std::string>& value) {
809 if (value.has_value()) {
810 sqlite3_bind_text(stmt, idx, value->c_str(), -1, SQLITE_TRANSIENT);
812 sqlite3_bind_null(stmt, idx);
817void bind_optional_timestamp(
820 const std::optional<std::chrono::system_clock::time_point>& tp) {
821 if (tp.has_value()) {
822 auto str = to_timestamp_string(tp.value());
823 sqlite3_bind_text(stmt, idx, str.c_str(), -1, SQLITE_TRANSIENT);
825 sqlite3_bind_null(stmt, idx);
830[[nodiscard]] std::string escape_json_string(
const std::string& str) {
831 std::ostringstream oss;
834 case '"': oss <<
"\\\"";
break;
835 case '\\': oss <<
"\\\\";
break;
836 case '\b': oss <<
"\\b";
break;
837 case '\f': oss <<
"\\f";
break;
838 case '\n': oss <<
"\\n";
break;
839 case '\r': oss <<
"\\r";
break;
840 case '\t': oss <<
"\\t";
break;
841 default: oss << c;
break;
848[[nodiscard]] std::string unescape_json_string(std::string_view str) {
850 result.reserve(str.size());
851 for (
size_t i = 0; i < str.size(); ++i) {
852 if (str[i] ==
'\\' && i + 1 < str.size()) {
855 case '"': result +=
'"';
break;
856 case '\\': result +=
'\\';
break;
857 case 'b': result +=
'\b';
break;
858 case 'f': result +=
'\f';
break;
859 case 'n': result +=
'\n';
break;
860 case 'r': result +=
'\r';
break;
861 case 't': result +=
'\t';
break;
862 default: result += str[i];
break;
872[[nodiscard]] std::pair<std::string, size_t> extract_json_string(
873 std::string_view json,
size_t pos) {
874 auto start = json.find(
'"', pos);
875 if (start == std::string_view::npos)
return {
"", std::string_view::npos};
877 size_t end = start + 1;
878 while (end < json.size()) {
879 if (json[end] ==
'\\' && end + 1 < json.size()) {
881 }
else if (json[end] ==
'"') {
888 if (end >= json.size())
return {
"", std::string_view::npos};
890 auto value = unescape_json_string(json.substr(start + 1, end - start - 1));
891 return {value, end + 1};
901 const std::vector<client::routing_condition>& conditions) {
902 if (conditions.empty())
return "[]";
904 std::ostringstream oss;
906 for (
size_t i = 0; i < conditions.size(); ++i) {
907 if (i > 0) oss <<
",";
908 const auto& cond = conditions[i];
911 oss <<
"\"pattern\":\"" << escape_json_string(cond.pattern) <<
"\",";
912 oss <<
"\"case_sensitive\":" << (cond.case_sensitive ?
"true" :
"false") <<
",";
913 oss <<
"\"negate\":" << (cond.negate ?
"true" :
"false");
921 std::string_view json) {
922 std::vector<client::routing_condition> result;
923 if (json.empty() || json ==
"[]")
return result;
926 while (pos < json.size()) {
927 auto obj_start = json.find(
'{', pos);
928 if (obj_start == std::string_view::npos)
break;
930 auto obj_end = json.find(
'}', obj_start);
931 if (obj_end == std::string_view::npos)
break;
933 auto obj = json.substr(obj_start, obj_end - obj_start + 1);
938 auto field_pos = obj.find(
"\"field\"");
939 if (field_pos != std::string_view::npos) {
940 auto [field_value, next] = extract_json_string(obj, field_pos + 7);
945 auto pattern_pos = obj.find(
"\"pattern\"");
946 if (pattern_pos != std::string_view::npos) {
947 auto [pattern_value, next] = extract_json_string(obj, pattern_pos + 9);
952 auto case_pos = obj.find(
"\"case_sensitive\"");
953 if (case_pos != std::string_view::npos) {
954 cond.
case_sensitive = (obj.find(
"true", case_pos) != std::string_view::npos &&
955 obj.find(
"true", case_pos) < obj.find(
',', case_pos));
959 auto negate_pos = obj.find(
"\"negate\"");
960 if (negate_pos != std::string_view::npos) {
961 cond.
negate = (obj.find(
"true", negate_pos) != std::string_view::npos);
964 result.push_back(std::move(cond));
976 const std::vector<client::routing_action>& actions) {
977 if (actions.empty())
return "[]";
979 std::ostringstream oss;
981 for (
size_t i = 0; i < actions.size(); ++i) {
982 if (i > 0) oss <<
",";
983 const auto& action = actions[i];
985 oss <<
"\"destination\":\"" << escape_json_string(action.destination_node_id) <<
"\",";
987 oss <<
"\"delay_minutes\":" << action.delay.count() <<
",";
988 oss <<
"\"delete_after_send\":" << (action.delete_after_send ?
"true" :
"false") <<
",";
989 oss <<
"\"notify_on_failure\":" << (action.notify_on_failure ?
"true" :
"false");
997 std::string_view json) {
998 std::vector<client::routing_action> result;
999 if (json.empty() || json ==
"[]")
return result;
1002 while (pos < json.size()) {
1003 auto obj_start = json.find(
'{', pos);
1004 if (obj_start == std::string_view::npos)
break;
1006 auto obj_end = json.find(
'}', obj_start);
1007 if (obj_end == std::string_view::npos)
break;
1009 auto obj = json.substr(obj_start, obj_end - obj_start + 1);
1014 auto dest_pos = obj.find(
"\"destination\"");
1015 if (dest_pos != std::string_view::npos) {
1016 auto [dest_value, next] = extract_json_string(obj, dest_pos + 13);
1021 auto prio_pos = obj.find(
"\"priority\"");
1022 if (prio_pos != std::string_view::npos) {
1023 auto [prio_value, next] = extract_json_string(obj, prio_pos + 10);
1028 auto delay_pos = obj.find(
"\"delay_minutes\"");
1029 if (delay_pos != std::string_view::npos) {
1030 auto colon = obj.find(
':', delay_pos);
1031 if (colon != std::string_view::npos) {
1033 std::sscanf(obj.data() + colon + 1,
"%d", &minutes);
1034 action.
delay = std::chrono::minutes{minutes};
1039 auto delete_pos = obj.find(
"\"delete_after_send\"");
1040 if (delete_pos != std::string_view::npos) {
1041 action.
delete_after_send = (obj.find(
"true", delete_pos) != std::string_view::npos &&
1042 obj.find(
"true", delete_pos) < obj.find(
',', delete_pos + 20));
1046 auto notify_pos = obj.find(
"\"notify_on_failure\"");
1047 if (notify_pos != std::string_view::npos) {
1048 action.
notify_on_failure = (obj.find(
"true", notify_pos) != std::string_view::npos);
1051 result.push_back(std::move(action));
1076 return VoidResult(kcenon::common::error_info{
1077 -1,
"Database not initialized",
"routing_repository"});
1080 static constexpr const char* sql = R
"(
1081 INSERT INTO routing_rules (
1082 rule_id, name, description, enabled, priority,
1083 conditions_json, actions_json,
1084 schedule_cron, effective_from, effective_until,
1085 triggered_count, success_count, failure_count,
1086 last_triggered, created_at, updated_at
1087 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1088 ON CONFLICT(rule_id) DO UPDATE SET
1089 name = excluded.name,
1090 description = excluded.description,
1091 enabled = excluded.enabled,
1092 priority = excluded.priority,
1093 conditions_json = excluded.conditions_json,
1094 actions_json = excluded.actions_json,
1095 schedule_cron = excluded.schedule_cron,
1096 effective_from = excluded.effective_from,
1097 effective_until = excluded.effective_until,
1098 updated_at = CURRENT_TIMESTAMP
1101 sqlite3_stmt* stmt = nullptr;
1102 if (sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1103 return VoidResult(kcenon::common::error_info{
1104 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
1105 "routing_repository"});
1109 sqlite3_bind_text(stmt, idx++, rule.rule_id.c_str(), -1, SQLITE_TRANSIENT);
1110 sqlite3_bind_text(stmt, idx++, rule.name.c_str(), -1, SQLITE_TRANSIENT);
1111 sqlite3_bind_text(stmt, idx++, rule.description.c_str(), -1, SQLITE_TRANSIENT);
1112 sqlite3_bind_int(stmt, idx++, rule.enabled ? 1 : 0);
1113 sqlite3_bind_int(stmt, idx++, rule.priority);
1115 auto conditions_json = serialize_conditions(rule.conditions);
1116 sqlite3_bind_text(stmt, idx++, conditions_json.c_str(), -1, SQLITE_TRANSIENT);
1118 auto actions_json = serialize_actions(rule.actions);
1119 sqlite3_bind_text(stmt, idx++, actions_json.c_str(), -1, SQLITE_TRANSIENT);
1121 bind_optional_text(stmt, idx++, rule.schedule_cron);
1122 bind_optional_timestamp(stmt, idx++, rule.effective_from);
1123 bind_optional_timestamp(stmt, idx++, rule.effective_until);
1125 sqlite3_bind_int64(stmt, idx++,
static_cast<int64_t
>(rule.triggered_count));
1126 sqlite3_bind_int64(stmt, idx++,
static_cast<int64_t
>(rule.success_count));
1127 sqlite3_bind_int64(stmt, idx++,
static_cast<int64_t
>(rule.failure_count));
1129 auto last_triggered_str = to_timestamp_string(rule.last_triggered);
1130 if (last_triggered_str.empty()) {
1131 sqlite3_bind_null(stmt, idx++);
1133 sqlite3_bind_text(stmt, idx++, last_triggered_str.c_str(), -1, SQLITE_TRANSIENT);
1136 auto created_str = to_timestamp_string(rule.created_at);
1137 if (created_str.empty()) {
1138 sqlite3_bind_text(stmt, idx++,
"CURRENT_TIMESTAMP", -1, SQLITE_STATIC);
1140 sqlite3_bind_text(stmt, idx++, created_str.c_str(), -1, SQLITE_TRANSIENT);
1143 auto updated_str = to_timestamp_string(rule.updated_at);
1144 if (updated_str.empty()) {
1145 sqlite3_bind_text(stmt, idx++,
"CURRENT_TIMESTAMP", -1, SQLITE_STATIC);
1147 sqlite3_bind_text(stmt, idx++, updated_str.c_str(), -1, SQLITE_TRANSIENT);
1150 auto rc = sqlite3_step(stmt);
1151 sqlite3_finalize(stmt);
1153 if (rc != SQLITE_DONE) {
1154 return VoidResult(kcenon::common::error_info{
1155 -1,
"Failed to save rule: " + std::string(sqlite3_errmsg(db_)),
1156 "routing_repository"});
1159 return kcenon::common::ok();
1163 std::string_view rule_id)
const {
1164 if (!
db_)
return std::nullopt;
1166 static constexpr const char* sql = R
"(
1167 SELECT pk, rule_id, name, description, enabled, priority,
1168 conditions_json, actions_json,
1169 schedule_cron, effective_from, effective_until,
1170 triggered_count, success_count, failure_count,
1171 last_triggered, created_at, updated_at
1172 FROM routing_rules WHERE rule_id = ?
1175 sqlite3_stmt* stmt = nullptr;
1176 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1177 return std::nullopt;
1180 sqlite3_bind_text(stmt, 1, rule_id.data(),
static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1182 std::optional<client::routing_rule> result;
1183 if (sqlite3_step(stmt) == SQLITE_ROW) {
1187 sqlite3_finalize(stmt);
1192 if (!
db_)
return std::nullopt;
1194 static constexpr const char* sql = R
"(
1195 SELECT pk, rule_id, name, description, enabled, priority,
1196 conditions_json, actions_json,
1197 schedule_cron, effective_from, effective_until,
1198 triggered_count, success_count, failure_count,
1199 last_triggered, created_at, updated_at
1200 FROM routing_rules WHERE pk = ?
1203 sqlite3_stmt* stmt = nullptr;
1204 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1205 return std::nullopt;
1208 sqlite3_bind_int64(stmt, 1, pk);
1210 std::optional<client::routing_rule> result;
1211 if (sqlite3_step(stmt) == SQLITE_ROW) {
1215 sqlite3_finalize(stmt);
1221 std::vector<client::routing_rule> result;
1222 if (!
db_)
return result;
1224 std::ostringstream sql;
1226 SELECT pk, rule_id, name, description, enabled, priority,
1227 conditions_json, actions_json,
1228 schedule_cron, effective_from, effective_until,
1229 triggered_count, success_count, failure_count,
1230 last_triggered, created_at, updated_at
1231 FROM routing_rules WHERE 1=1
1234 if (options.enabled_only.has_value()) {
1235 sql <<
" AND enabled = " << (options.enabled_only.value() ?
"1" :
"0");
1238 if (options.order_by_priority) {
1239 sql <<
" ORDER BY priority DESC, created_at ASC";
1241 sql <<
" ORDER BY created_at DESC";
1244 sql <<
" LIMIT " << options.limit <<
" OFFSET " << options.offset;
1246 sqlite3_stmt* stmt =
nullptr;
1247 auto sql_str = sql.str();
1248 if (sqlite3_prepare_v2(
db_, sql_str.c_str(), -1, &stmt,
nullptr) != SQLITE_OK) {
1252 while (sqlite3_step(stmt) == SQLITE_ROW) {
1256 sqlite3_finalize(stmt);
1263 options.order_by_priority =
true;
1269 return VoidResult(kcenon::common::error_info{
1270 -1,
"Database not initialized",
"routing_repository"});
1273 static constexpr const char* sql =
"DELETE FROM routing_rules WHERE rule_id = ?";
1275 sqlite3_stmt* stmt =
nullptr;
1276 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1277 return VoidResult(kcenon::common::error_info{
1278 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(
db_)),
1279 "routing_repository"});
1282 sqlite3_bind_text(stmt, 1, rule_id.data(),
static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1284 auto rc = sqlite3_step(stmt);
1285 sqlite3_finalize(stmt);
1287 if (rc != SQLITE_DONE) {
1288 return VoidResult(kcenon::common::error_info{
1289 -1,
"Failed to delete rule: " + std::string(sqlite3_errmsg(
db_)),
1290 "routing_repository"});
1293 return kcenon::common::ok();
1297 if (!
db_)
return false;
1299 static constexpr const char* sql =
"SELECT 1 FROM routing_rules WHERE rule_id = ?";
1301 sqlite3_stmt* stmt =
nullptr;
1302 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1306 sqlite3_bind_text(stmt, 1, rule_id.data(),
static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1308 bool found = (sqlite3_step(stmt) == SQLITE_ROW);
1309 sqlite3_finalize(stmt);
1319 return VoidResult(kcenon::common::error_info{
1320 -1,
"Database not initialized",
"routing_repository"});
1323 static constexpr const char* sql = R
"(
1324 UPDATE routing_rules SET
1326 updated_at = CURRENT_TIMESTAMP
1330 sqlite3_stmt* stmt = nullptr;
1331 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1332 return VoidResult(kcenon::common::error_info{
1333 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(
db_)),
1334 "routing_repository"});
1337 sqlite3_bind_int(stmt, 1, priority);
1338 sqlite3_bind_text(stmt, 2, rule_id.data(),
static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1340 auto rc = sqlite3_step(stmt);
1341 sqlite3_finalize(stmt);
1343 if (rc != SQLITE_DONE) {
1344 return VoidResult(kcenon::common::error_info{
1345 -1,
"Failed to update priority: " + std::string(sqlite3_errmsg(
db_)),
1346 "routing_repository"});
1349 return kcenon::common::ok();
1354 return VoidResult(kcenon::common::error_info{
1355 -1,
"Database not initialized",
"routing_repository"});
1358 static constexpr const char* sql = R
"(
1359 UPDATE routing_rules SET
1361 updated_at = CURRENT_TIMESTAMP
1365 sqlite3_stmt* stmt = nullptr;
1366 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1367 return VoidResult(kcenon::common::error_info{
1368 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(
db_)),
1369 "routing_repository"});
1372 sqlite3_bind_text(stmt, 1, rule_id.data(),
static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1374 auto rc = sqlite3_step(stmt);
1375 sqlite3_finalize(stmt);
1377 if (rc != SQLITE_DONE) {
1378 return VoidResult(kcenon::common::error_info{
1379 -1,
"Failed to enable rule: " + std::string(sqlite3_errmsg(
db_)),
1380 "routing_repository"});
1383 return kcenon::common::ok();
1388 return VoidResult(kcenon::common::error_info{
1389 -1,
"Database not initialized",
"routing_repository"});
1392 static constexpr const char* sql = R
"(
1393 UPDATE routing_rules SET
1395 updated_at = CURRENT_TIMESTAMP
1399 sqlite3_stmt* stmt = nullptr;
1400 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1401 return VoidResult(kcenon::common::error_info{
1402 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(
db_)),
1403 "routing_repository"});
1406 sqlite3_bind_text(stmt, 1, rule_id.data(),
static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1408 auto rc = sqlite3_step(stmt);
1409 sqlite3_finalize(stmt);
1411 if (rc != SQLITE_DONE) {
1412 return VoidResult(kcenon::common::error_info{
1413 -1,
"Failed to disable rule: " + std::string(sqlite3_errmsg(
db_)),
1414 "routing_repository"});
1417 return kcenon::common::ok();
1426 return VoidResult(kcenon::common::error_info{
1427 -1,
"Database not initialized",
"routing_repository"});
1430 static constexpr const char* sql = R
"(
1431 UPDATE routing_rules SET
1432 triggered_count = triggered_count + 1,
1433 last_triggered = CURRENT_TIMESTAMP
1437 sqlite3_stmt* stmt = nullptr;
1438 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1439 return VoidResult(kcenon::common::error_info{
1440 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(
db_)),
1441 "routing_repository"});
1444 sqlite3_bind_text(stmt, 1, rule_id.data(),
static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1446 auto rc = sqlite3_step(stmt);
1447 sqlite3_finalize(stmt);
1449 if (rc != SQLITE_DONE) {
1450 return VoidResult(kcenon::common::error_info{
1451 -1,
"Failed to increment triggered: " + std::string(sqlite3_errmsg(
db_)),
1452 "routing_repository"});
1455 return kcenon::common::ok();
1460 return VoidResult(kcenon::common::error_info{
1461 -1,
"Database not initialized",
"routing_repository"});
1464 static constexpr const char* sql = R
"(
1465 UPDATE routing_rules SET success_count = success_count + 1 WHERE rule_id = ?
1468 sqlite3_stmt* stmt = nullptr;
1469 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1470 return VoidResult(kcenon::common::error_info{
1471 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(
db_)),
1472 "routing_repository"});
1475 sqlite3_bind_text(stmt, 1, rule_id.data(),
static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1477 auto rc = sqlite3_step(stmt);
1478 sqlite3_finalize(stmt);
1480 if (rc != SQLITE_DONE) {
1481 return VoidResult(kcenon::common::error_info{
1482 -1,
"Failed to increment success: " + std::string(sqlite3_errmsg(
db_)),
1483 "routing_repository"});
1486 return kcenon::common::ok();
1491 return VoidResult(kcenon::common::error_info{
1492 -1,
"Database not initialized",
"routing_repository"});
1495 static constexpr const char* sql = R
"(
1496 UPDATE routing_rules SET failure_count = failure_count + 1 WHERE rule_id = ?
1499 sqlite3_stmt* stmt = nullptr;
1500 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1501 return VoidResult(kcenon::common::error_info{
1502 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(
db_)),
1503 "routing_repository"});
1506 sqlite3_bind_text(stmt, 1, rule_id.data(),
static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1508 auto rc = sqlite3_step(stmt);
1509 sqlite3_finalize(stmt);
1511 if (rc != SQLITE_DONE) {
1512 return VoidResult(kcenon::common::error_info{
1513 -1,
"Failed to increment failure: " + std::string(sqlite3_errmsg(
db_)),
1514 "routing_repository"});
1517 return kcenon::common::ok();
1522 return VoidResult(kcenon::common::error_info{
1523 -1,
"Database not initialized",
"routing_repository"});
1526 static constexpr const char* sql = R
"(
1527 UPDATE routing_rules SET
1528 triggered_count = 0,
1531 last_triggered = NULL
1535 sqlite3_stmt* stmt = nullptr;
1536 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1537 return VoidResult(kcenon::common::error_info{
1538 -1,
"Failed to prepare statement: " + std::string(sqlite3_errmsg(
db_)),
1539 "routing_repository"});
1542 sqlite3_bind_text(stmt, 1, rule_id.data(),
static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1544 auto rc = sqlite3_step(stmt);
1545 sqlite3_finalize(stmt);
1547 if (rc != SQLITE_DONE) {
1548 return VoidResult(kcenon::common::error_info{
1549 -1,
"Failed to reset statistics: " + std::string(sqlite3_errmsg(
db_)),
1550 "routing_repository"});
1553 return kcenon::common::ok();
1559 static constexpr const char* sql =
"SELECT COUNT(*) FROM routing_rules";
1561 sqlite3_stmt* stmt =
nullptr;
1562 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1567 if (sqlite3_step(stmt) == SQLITE_ROW) {
1568 result =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1571 sqlite3_finalize(stmt);
1578 static constexpr const char* sql =
"SELECT COUNT(*) FROM routing_rules WHERE enabled = 1";
1580 sqlite3_stmt* stmt =
nullptr;
1581 if (sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr) != SQLITE_OK) {
1586 if (sqlite3_step(stmt) == SQLITE_ROW) {
1587 result =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1590 sqlite3_finalize(stmt);
1607 auto* stmt =
static_cast<sqlite3_stmt*
>(stmt_ptr);
1611 rule.
pk = get_int64_column(stmt, col++);
1612 rule.
rule_id = get_text_column(stmt, col++);
1613 rule.
name = get_text_column(stmt, col++);
1615 rule.
enabled = (get_int_column(stmt, col++) != 0);
1616 rule.
priority = get_int_column(stmt, col++);
1618 auto conditions_json = get_text_column(stmt, col++);
1621 auto actions_json = get_text_column(stmt, col++);
1626 auto effective_from_str = get_text_column(stmt, col++);
1627 rule.
effective_from = from_optional_timestamp(effective_from_str.c_str());
1629 auto effective_until_str = get_text_column(stmt, col++);
1630 rule.
effective_until = from_optional_timestamp(effective_until_str.c_str());
1632 rule.
triggered_count =
static_cast<size_t>(get_int64_column(stmt, col++));
1633 rule.
success_count =
static_cast<size_t>(get_int64_column(stmt, col++));
1634 rule.
failure_count =
static_cast<size_t>(get_int64_column(stmt, col++));
1636 auto last_triggered_str = get_text_column(stmt, col++);
1637 rule.
last_triggered = from_timestamp_string(last_triggered_str.c_str());
1639 auto created_str = get_text_column(stmt, col++);
1640 rule.
created_at = from_timestamp_string(created_str.c_str());
1642 auto updated_str = get_text_column(stmt, col++);
1643 rule.
updated_at = from_timestamp_string(updated_str.c_str());
Repository for routing rule persistence (legacy SQLite interface)
auto disable_rule(std::string_view rule_id) -> VoidResult
auto exists(std::string_view rule_id) const -> bool
static auto serialize_conditions(const std::vector< client::routing_condition > &conditions) -> std::string
routing_repository(sqlite3 *db)
auto remove(std::string_view rule_id) -> VoidResult
auto find_by_id(std::string_view rule_id) const -> std::optional< client::routing_rule >
auto find_rules(const routing_rule_query_options &options={}) const -> std::vector< client::routing_rule >
auto count_enabled() const -> size_t
auto is_valid() const noexcept -> bool
auto parse_row(void *stmt) const -> client::routing_rule
auto reset_statistics(std::string_view rule_id) -> VoidResult
auto find_by_pk(int64_t pk) const -> std::optional< client::routing_rule >
static auto deserialize_actions(std::string_view json) -> std::vector< client::routing_action >
static auto deserialize_conditions(std::string_view json) -> std::vector< client::routing_condition >
auto count() const -> size_t
static auto serialize_actions(const std::vector< client::routing_action > &actions) -> std::string
auto increment_triggered(std::string_view rule_id) -> VoidResult
auto enable_rule(std::string_view rule_id) -> VoidResult
auto increment_failure(std::string_view rule_id) -> VoidResult
auto increment_success(std::string_view rule_id) -> VoidResult
auto update_priority(std::string_view rule_id, int priority) -> VoidResult
auto find_enabled_rules() const -> std::vector< client::routing_rule >
constexpr const char * to_string(job_type type) noexcept
Convert job_type to string representation.
routing_field routing_field_from_string(std::string_view str) noexcept
Parse routing_field from string.
job_priority job_priority_from_string(std::string_view str) noexcept
Parse job_priority from string.
@ move
C-MOVE move request/response.
Repository for routing rule persistence using base_repository pattern.
Action to perform when a routing rule matches.
std::string destination_node_id
Target remote node ID.
std::chrono::minutes delay
Delay before forwarding.
bool notify_on_failure
Generate notification on failure.
job_priority priority
Job priority for forwarding.
bool delete_after_send
Delete local copy after successful send.
A single condition for routing rule evaluation.
std::string pattern
Pattern to match (supports wildcards: *, ?)
routing_field match_field
The DICOM field to match.
bool negate
Invert the match result.
bool case_sensitive
Whether matching is case-sensitive.
A complete routing rule with conditions and actions.
size_t success_count
Successful forwarding count.
std::string name
Human-readable name.
std::optional< std::string > schedule_cron
Cron expression for scheduling.
std::optional< std::chrono::system_clock::time_point > effective_until
std::optional< std::chrono::system_clock::time_point > effective_from
std::chrono::system_clock::time_point updated_at
std::string rule_id
Unique rule identifier.
bool enabled
Whether the rule is active.
std::chrono::system_clock::time_point created_at
std::vector< routing_condition > conditions
Conditions (AND logic)
std::chrono::system_clock::time_point last_triggered
std::string description
Detailed description.
size_t triggered_count
Number of times the rule was triggered.
int64_t pk
Primary key (0 if not persisted)
size_t failure_count
Failed forwarding count.
std::vector< routing_action > actions
Actions to execute on match.
int priority
Evaluation priority (higher = first)
Query options for listing routing rules.
std::optional< bool > enabled_only
Filter by enabled status.