PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
routing_repository.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
16
17#include <chrono>
18#include <cstring>
19#include <iomanip>
20#include <sstream>
21
22#ifdef PACS_WITH_DATABASE_SYSTEM
23
24namespace kcenon::pacs::storage {
25
26// =============================================================================
27// Constructor
28// =============================================================================
29
30routing_repository::routing_repository(std::shared_ptr<pacs_database_adapter> db)
31 : base_repository(std::move(db), "routing_rules", "rule_id") {}
32
33// =============================================================================
34// JSON Serialization (Static methods)
35// =============================================================================
36
37namespace {
38
40[[nodiscard]] std::string escape_json_string(const std::string& str) {
41 std::ostringstream oss;
42 for (char c : str) {
43 switch (c) {
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;
52 }
53 }
54 return oss.str();
55}
56
58[[nodiscard]] std::string unescape_json_string(std::string_view str) {
59 std::string result;
60 result.reserve(str.size());
61 for (size_t i = 0; i < str.size(); ++i) {
62 if (str[i] == '\\' && i + 1 < str.size()) {
63 ++i;
64 switch (str[i]) {
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;
73 }
74 } else {
75 result += str[i];
76 }
77 }
78 return result;
79}
80
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};
86
87 size_t end = start + 1;
88 while (end < json.size()) {
89 if (json[end] == '\\' && end + 1 < json.size()) {
90 end += 2;
91 } else if (json[end] == '"') {
92 break;
93 } else {
94 ++end;
95 }
96 }
97
98 if (end >= json.size()) return {"", std::string_view::npos};
99
100 auto value = unescape_json_string(json.substr(start + 1, end - start - 1));
101 return {value, end + 1};
102}
103
104} // namespace
105
106std::string routing_repository::serialize_conditions(
107 const std::vector<client::routing_condition>& conditions) {
108 if (conditions.empty()) return "[]";
109
110 std::ostringstream oss;
111 oss << "[";
112 for (size_t i = 0; i < conditions.size(); ++i) {
113 if (i > 0) oss << ",";
114 const auto& cond = conditions[i];
115 oss << "{";
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");
120 oss << "}";
121 }
122 oss << "]";
123 return oss.str();
124}
125
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;
130
131 size_t pos = 0;
132 while (pos < json.size()) {
133 auto obj_start = json.find('{', pos);
134 if (obj_start == std::string_view::npos) break;
135
136 auto obj_end = json.find('}', obj_start);
137 if (obj_end == std::string_view::npos) break;
138
139 auto obj = json.substr(obj_start, obj_end - obj_start + 1);
140
141 client::routing_condition cond;
142
143 // Parse field
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);
148 }
149
150 // Parse pattern
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;
155 }
156
157 // Parse case_sensitive
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));
162 }
163
164 // Parse negate
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);
168 }
169
170 result.push_back(std::move(cond));
171 pos = obj_end + 1;
172 }
173
174 return result;
175}
176
177std::string routing_repository::serialize_actions(
178 const std::vector<client::routing_action>& actions) {
179 if (actions.empty()) return "[]";
180
181 std::ostringstream oss;
182 oss << "[";
183 for (size_t i = 0; i < actions.size(); ++i) {
184 if (i > 0) oss << ",";
185 const auto& action = actions[i];
186 oss << "{";
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");
192 oss << "}";
193 }
194 oss << "]";
195 return oss.str();
196}
197
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;
202
203 size_t pos = 0;
204 while (pos < json.size()) {
205 auto obj_start = json.find('{', pos);
206 if (obj_start == std::string_view::npos) break;
207
208 auto obj_end = json.find('}', obj_start);
209 if (obj_end == std::string_view::npos) break;
210
211 auto obj = json.substr(obj_start, obj_end - obj_start + 1);
212
213 client::routing_action action;
214
215 // Parse destination
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;
220 }
221
222 // Parse priority
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);
227 }
228
229 // Parse delay_minutes
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) {
234 int minutes = 0;
235 std::sscanf(obj.data() + colon + 1, "%d", &minutes);
236 action.delay = std::chrono::minutes{minutes};
237 }
238 }
239
240 // Parse delete_after_send
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));
245 }
246
247 // Parse notify_on_failure
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);
251 }
252
253 result.push_back(std::move(action));
254 pos = obj_end + 1;
255 }
256
257 return result;
258}
259
260// =============================================================================
261// Timestamp Helpers
262// =============================================================================
263
264auto routing_repository::parse_timestamp(const std::string& str) const
265 -> std::chrono::system_clock::time_point {
266 if (str.empty()) {
267 return {};
268 }
269
270 std::tm tm{};
271 std::istringstream ss(str);
272 ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
273 if (ss.fail()) {
274 return {};
275 }
276
277#ifdef _WIN32
278 auto time = _mkgmtime(&tm);
279#else
280 auto time = timegm(&tm);
281#endif
282
283 return std::chrono::system_clock::from_time_t(time);
284}
285
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{}) {
289 return "";
290 }
291
292 auto time = std::chrono::system_clock::to_time_t(tp);
293 std::tm tm{};
294#ifdef _WIN32
295 gmtime_s(&tm, &time);
296#else
297 gmtime_r(&time, &tm);
298#endif
299
300 char buf[32];
301 std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
302 return buf;
303}
304
305auto routing_repository::format_optional_timestamp(
306 const std::optional<std::chrono::system_clock::time_point>& tp) const
307 -> std::string {
308 if (!tp.has_value()) {
309 return "";
310 }
311 return format_timestamp(tp.value());
312}
313
314// =============================================================================
315// Domain-Specific Operations
316// =============================================================================
317
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"});
322 }
323
324 auto builder = query_builder();
325 builder.select(select_columns())
326 .from(table_name())
327 .where("pk", "=", pk)
328 .limit(1);
329
330 auto result = storage_session().select(builder.build());
331 if (result.is_err()) {
332 return result_type(result.error());
333 }
334
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"});
338 }
339
340 return result_type(map_row_to_entity(result.value()[0]));
341}
342
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"});
348 }
349
350 auto builder = query_builder();
351 builder.select(select_columns()).from(table_name());
352
353 if (options.enabled_only.has_value()) {
354 builder.where("enabled", "=", options.enabled_only.value() ? 1 : 0);
355 }
356
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);
360 } else {
361 builder.order_by("created_at", database::sort_order::desc);
362 }
363
364 builder.limit(options.limit).offset(options.offset);
365
366 auto result = storage_session().select(builder.build());
367 if (result.is_err()) {
368 return list_result_type(result.error());
369 }
370
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));
375 }
376
377 return list_result_type(std::move(rules));
378}
379
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);
385}
386
387// =============================================================================
388// Rule Ordering
389// =============================================================================
390
391auto routing_repository::update_priority(std::string_view rule_id, int priority)
392 -> VoidResult {
393 if (!db() || !db()->is_connected()) {
394 return VoidResult(kcenon::common::error_info{
395 -1, "Database not connected", "storage"});
396 }
397
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));
402
403 auto result = storage_session().execute(builder.build());
404 if (result.is_err()) {
405 return VoidResult(result.error());
406 }
407
408 return kcenon::common::ok();
409}
410
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"});
415 }
416
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));
421
422 auto result = storage_session().execute(builder.build());
423 if (result.is_err()) {
424 return VoidResult(result.error());
425 }
426
427 return kcenon::common::ok();
428}
429
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"});
434 }
435
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));
440
441 auto result = storage_session().execute(builder.build());
442 if (result.is_err()) {
443 return VoidResult(result.error());
444 }
445
446 return kcenon::common::ok();
447}
448
449// =============================================================================
450// Statistics
451// =============================================================================
452
453auto routing_repository::increment_triggered(std::string_view rule_id)
454 -> VoidResult {
455 if (!db() || !db()->is_connected()) {
456 return VoidResult(kcenon::common::error_info{
457 -1, "Database not connected", "storage"});
458 }
459
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) + "'";
464
465 auto result = storage_session().execute(sql);
466 if (result.is_err()) {
467 return VoidResult(result.error());
468 }
469
470 return kcenon::common::ok();
471}
472
473auto routing_repository::increment_success(std::string_view rule_id)
474 -> VoidResult {
475 if (!db() || !db()->is_connected()) {
476 return VoidResult(kcenon::common::error_info{
477 -1, "Database not connected", "storage"});
478 }
479
480 auto sql = "UPDATE " + table_name() +
481 " SET success_count = success_count + 1 WHERE rule_id = '" +
482 std::string(rule_id) + "'";
483
484 auto result = storage_session().execute(sql);
485 if (result.is_err()) {
486 return VoidResult(result.error());
487 }
488
489 return kcenon::common::ok();
490}
491
492auto routing_repository::increment_failure(std::string_view rule_id)
493 -> VoidResult {
494 if (!db() || !db()->is_connected()) {
495 return VoidResult(kcenon::common::error_info{
496 -1, "Database not connected", "storage"});
497 }
498
499 auto sql = "UPDATE " + table_name() +
500 " SET failure_count = failure_count + 1 WHERE rule_id = '" +
501 std::string(rule_id) + "'";
502
503 auto result = storage_session().execute(sql);
504 if (result.is_err()) {
505 return VoidResult(result.error());
506 }
507
508 return kcenon::common::ok();
509}
510
511auto routing_repository::reset_statistics(std::string_view rule_id)
512 -> VoidResult {
513 if (!db() || !db()->is_connected()) {
514 return VoidResult(kcenon::common::error_info{
515 -1, "Database not connected", "storage"});
516 }
517
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));
525
526 auto result = storage_session().execute(builder.build());
527 if (result.is_err()) {
528 return VoidResult(result.error());
529 }
530
531 return kcenon::common::ok();
532}
533
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"});
538 }
539
540 auto builder = query_builder();
541 builder.select({"COUNT(*)"})
542 .from(table_name())
543 .where("enabled", "=", 1);
544
545 auto result = storage_session().select(builder.build());
546 if (result.is_err()) {
547 return Result<size_t>(result.error());
548 }
549
550 if (result.value().empty() || result.value()[0].empty()) {
551 return Result<size_t>(static_cast<size_t>(0));
552 }
553
554 const auto& row = result.value()[0];
555 auto it = row.find("COUNT(*)");
556 if (it == row.end()) {
557 it = row.begin();
558 }
559
560 if (it != row.end() && !it->second.empty()) {
561 return Result<size_t>(static_cast<size_t>(std::stoull(it->second)));
562 }
563
564 return Result<size_t>(static_cast<size_t>(0));
565}
566
567// =============================================================================
568// base_repository overrides
569// =============================================================================
570
571auto routing_repository::map_row_to_entity(const database_row& row) const
572 -> client::routing_rule {
573 client::routing_rule rule;
574
575 // Parse pk if present
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);
579 }
580
581 rule.rule_id = row.at("rule_id");
582 rule.name = row.at("name");
583
584 auto desc_it = row.find("description");
585 if (desc_it != row.end()) {
586 rule.description = desc_it->second;
587 }
588
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);
592 }
593
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);
597 }
598
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);
602 }
603
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);
607 }
608
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;
612 }
613
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;
619 }
620 }
621
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;
627 }
628 }
629
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));
633 }
634
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));
638 }
639
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));
643 }
644
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);
648 }
649
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);
653 }
654
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);
658 }
659
660 return rule;
661}
662
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;
666
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);
683
684 return row;
685}
686
687auto routing_repository::get_pk(const client::routing_rule& entity) const
688 -> std::string {
689 return entity.rule_id;
690}
691
692auto routing_repository::has_pk(const client::routing_rule& entity) const
693 -> bool {
694 return !entity.rule_id.empty();
695}
696
697auto routing_repository::select_columns() const -> std::vector<std::string> {
698 return {
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"
704 };
705}
706
707} // namespace kcenon::pacs::storage
708
709#else // !PACS_WITH_DATABASE_SYSTEM
710
711// =============================================================================
712// Legacy SQLite Implementation
713// =============================================================================
714
715#include <sqlite3.h>
716
717namespace kcenon::pacs::storage {
718
719// =============================================================================
720// Helper Functions
721// =============================================================================
722
723namespace {
724
726[[nodiscard]] std::string to_timestamp_string(
727 std::chrono::system_clock::time_point tp) {
728 if (tp == std::chrono::system_clock::time_point{}) {
729 return "";
730 }
731 auto time = std::chrono::system_clock::to_time_t(tp);
732 std::tm tm{};
733#ifdef _WIN32
734 gmtime_s(&tm, &time);
735#else
736 gmtime_r(&time, &tm);
737#endif
738 char buf[32];
739 std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
740 return buf;
741}
742
744[[nodiscard]] std::chrono::system_clock::time_point from_timestamp_string(
745 const char* str) {
746 if (!str || str[0] == '\0') {
747 return {};
748 }
749 std::tm tm{};
750 std::istringstream ss(str);
751 ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
752 if (ss.fail()) {
753 return {};
754 }
755#ifdef _WIN32
756 auto time = _mkgmtime(&tm);
757#else
758 auto time = timegm(&tm);
759#endif
760 return std::chrono::system_clock::from_time_t(time);
761}
762
764[[nodiscard]] std::optional<std::chrono::system_clock::time_point>
765from_optional_timestamp(const char* str) {
766 if (!str || str[0] == '\0') {
767 return std::nullopt;
768 }
769 auto tp = from_timestamp_string(str);
770 if (tp == std::chrono::system_clock::time_point{}) {
771 return std::nullopt;
772 }
773 return tp;
774}
775
777[[nodiscard]] std::string get_text_column(sqlite3_stmt* stmt, int col) {
778 auto text = reinterpret_cast<const char*>(sqlite3_column_text(stmt, col));
779 return text ? text : "";
780}
781
783[[nodiscard]] int get_int_column(sqlite3_stmt* stmt, int col, int default_val = 0) {
784 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
785 return default_val;
786 }
787 return sqlite3_column_int(stmt, col);
788}
789
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) {
793 return default_val;
794 }
795 return sqlite3_column_int64(stmt, col);
796}
797
799[[nodiscard]] std::optional<std::string> get_optional_text(sqlite3_stmt* stmt, int col) {
800 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
801 return std::nullopt;
802 }
803 auto text = reinterpret_cast<const char*>(sqlite3_column_text(stmt, col));
804 return text ? std::optional<std::string>{text} : std::nullopt;
805}
806
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);
811 } else {
812 sqlite3_bind_null(stmt, idx);
813 }
814}
815
817void bind_optional_timestamp(
818 sqlite3_stmt* stmt,
819 int idx,
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);
824 } else {
825 sqlite3_bind_null(stmt, idx);
826 }
827}
828
830[[nodiscard]] std::string escape_json_string(const std::string& str) {
831 std::ostringstream oss;
832 for (char c : str) {
833 switch (c) {
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;
842 }
843 }
844 return oss.str();
845}
846
848[[nodiscard]] std::string unescape_json_string(std::string_view str) {
849 std::string result;
850 result.reserve(str.size());
851 for (size_t i = 0; i < str.size(); ++i) {
852 if (str[i] == '\\' && i + 1 < str.size()) {
853 ++i;
854 switch (str[i]) {
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;
863 }
864 } else {
865 result += str[i];
866 }
867 }
868 return result;
869}
870
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};
876
877 size_t end = start + 1;
878 while (end < json.size()) {
879 if (json[end] == '\\' && end + 1 < json.size()) {
880 end += 2;
881 } else if (json[end] == '"') {
882 break;
883 } else {
884 ++end;
885 }
886 }
887
888 if (end >= json.size()) return {"", std::string_view::npos};
889
890 auto value = unescape_json_string(json.substr(start + 1, end - start - 1));
891 return {value, end + 1};
892}
893
894} // namespace
895
896// =============================================================================
897// JSON Serialization for Conditions
898// =============================================================================
899
901 const std::vector<client::routing_condition>& conditions) {
902 if (conditions.empty()) return "[]";
903
904 std::ostringstream oss;
905 oss << "[";
906 for (size_t i = 0; i < conditions.size(); ++i) {
907 if (i > 0) oss << ",";
908 const auto& cond = conditions[i];
909 oss << "{";
910 oss << "\"field\":\"" << client::to_string(cond.match_field) << "\",";
911 oss << "\"pattern\":\"" << escape_json_string(cond.pattern) << "\",";
912 oss << "\"case_sensitive\":" << (cond.case_sensitive ? "true" : "false") << ",";
913 oss << "\"negate\":" << (cond.negate ? "true" : "false");
914 oss << "}";
915 }
916 oss << "]";
917 return oss.str();
918}
919
920std::vector<client::routing_condition> routing_repository::deserialize_conditions(
921 std::string_view json) {
922 std::vector<client::routing_condition> result;
923 if (json.empty() || json == "[]") return result;
924
925 size_t pos = 0;
926 while (pos < json.size()) {
927 auto obj_start = json.find('{', pos);
928 if (obj_start == std::string_view::npos) break;
929
930 auto obj_end = json.find('}', obj_start);
931 if (obj_end == std::string_view::npos) break;
932
933 auto obj = json.substr(obj_start, obj_end - obj_start + 1);
934
936
937 // Parse field
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);
942 }
943
944 // Parse pattern
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);
948 cond.pattern = pattern_value;
949 }
950
951 // Parse case_sensitive
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));
956 }
957
958 // Parse negate
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);
962 }
963
964 result.push_back(std::move(cond));
965 pos = obj_end + 1;
966 }
967
968 return result;
969}
970
971// =============================================================================
972// JSON Serialization for Actions
973// =============================================================================
974
976 const std::vector<client::routing_action>& actions) {
977 if (actions.empty()) return "[]";
978
979 std::ostringstream oss;
980 oss << "[";
981 for (size_t i = 0; i < actions.size(); ++i) {
982 if (i > 0) oss << ",";
983 const auto& action = actions[i];
984 oss << "{";
985 oss << "\"destination\":\"" << escape_json_string(action.destination_node_id) << "\",";
986 oss << "\"priority\":\"" << client::to_string(action.priority) << "\",";
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");
990 oss << "}";
991 }
992 oss << "]";
993 return oss.str();
994}
995
996std::vector<client::routing_action> routing_repository::deserialize_actions(
997 std::string_view json) {
998 std::vector<client::routing_action> result;
999 if (json.empty() || json == "[]") return result;
1000
1001 size_t pos = 0;
1002 while (pos < json.size()) {
1003 auto obj_start = json.find('{', pos);
1004 if (obj_start == std::string_view::npos) break;
1005
1006 auto obj_end = json.find('}', obj_start);
1007 if (obj_end == std::string_view::npos) break;
1008
1009 auto obj = json.substr(obj_start, obj_end - obj_start + 1);
1010
1012
1013 // Parse destination
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);
1017 action.destination_node_id = dest_value;
1018 }
1019
1020 // Parse priority
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);
1024 action.priority = client::job_priority_from_string(prio_value);
1025 }
1026
1027 // Parse delay_minutes
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) {
1032 int minutes = 0;
1033 std::sscanf(obj.data() + colon + 1, "%d", &minutes);
1034 action.delay = std::chrono::minutes{minutes};
1035 }
1036 }
1037
1038 // Parse delete_after_send
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));
1043 }
1044
1045 // Parse notify_on_failure
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);
1049 }
1050
1051 result.push_back(std::move(action));
1052 pos = obj_end + 1;
1053 }
1054
1055 return result;
1056}
1057
1058// =============================================================================
1059// Construction / Destruction
1060// =============================================================================
1061
1063
1065
1067
1068auto routing_repository::operator=(routing_repository&&) noexcept -> routing_repository& = default;
1069
1070// =============================================================================
1071// CRUD Operations
1072// =============================================================================
1073
1074VoidResult routing_repository::save(const client::routing_rule& rule) {
1075 if (!db_) {
1076 return VoidResult(kcenon::common::error_info{
1077 -1, "Database not initialized", "routing_repository"});
1078 }
1079
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
1099 )";
1100
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"});
1106 }
1107
1108 int idx = 1;
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);
1114
1115 auto conditions_json = serialize_conditions(rule.conditions);
1116 sqlite3_bind_text(stmt, idx++, conditions_json.c_str(), -1, SQLITE_TRANSIENT);
1117
1118 auto actions_json = serialize_actions(rule.actions);
1119 sqlite3_bind_text(stmt, idx++, actions_json.c_str(), -1, SQLITE_TRANSIENT);
1120
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);
1124
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));
1128
1129 auto last_triggered_str = to_timestamp_string(rule.last_triggered);
1130 if (last_triggered_str.empty()) {
1131 sqlite3_bind_null(stmt, idx++);
1132 } else {
1133 sqlite3_bind_text(stmt, idx++, last_triggered_str.c_str(), -1, SQLITE_TRANSIENT);
1134 }
1135
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);
1139 } else {
1140 sqlite3_bind_text(stmt, idx++, created_str.c_str(), -1, SQLITE_TRANSIENT);
1141 }
1142
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);
1146 } else {
1147 sqlite3_bind_text(stmt, idx++, updated_str.c_str(), -1, SQLITE_TRANSIENT);
1148 }
1149
1150 auto rc = sqlite3_step(stmt);
1151 sqlite3_finalize(stmt);
1152
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"});
1157 }
1158
1159 return kcenon::common::ok();
1160}
1161
1162std::optional<client::routing_rule> routing_repository::find_by_id(
1163 std::string_view rule_id) const {
1164 if (!db_) return std::nullopt;
1165
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 = ?
1173 )";
1174
1175 sqlite3_stmt* stmt = nullptr;
1176 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
1177 return std::nullopt;
1178 }
1179
1180 sqlite3_bind_text(stmt, 1, rule_id.data(), static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1181
1182 std::optional<client::routing_rule> result;
1183 if (sqlite3_step(stmt) == SQLITE_ROW) {
1184 result = parse_row(stmt);
1185 }
1186
1187 sqlite3_finalize(stmt);
1188 return result;
1189}
1190
1191std::optional<client::routing_rule> routing_repository::find_by_pk(int64_t pk) const {
1192 if (!db_) return std::nullopt;
1193
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 = ?
1201 )";
1202
1203 sqlite3_stmt* stmt = nullptr;
1204 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
1205 return std::nullopt;
1206 }
1207
1208 sqlite3_bind_int64(stmt, 1, pk);
1209
1210 std::optional<client::routing_rule> result;
1211 if (sqlite3_step(stmt) == SQLITE_ROW) {
1212 result = parse_row(stmt);
1213 }
1214
1215 sqlite3_finalize(stmt);
1216 return result;
1217}
1218
1219std::vector<client::routing_rule> routing_repository::find_rules(
1220 const routing_rule_query_options& options) const {
1221 std::vector<client::routing_rule> result;
1222 if (!db_) return result;
1223
1224 std::ostringstream sql;
1225 sql << R"(
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
1232 )";
1233
1234 if (options.enabled_only.has_value()) {
1235 sql << " AND enabled = " << (options.enabled_only.value() ? "1" : "0");
1236 }
1237
1238 if (options.order_by_priority) {
1239 sql << " ORDER BY priority DESC, created_at ASC";
1240 } else {
1241 sql << " ORDER BY created_at DESC";
1242 }
1243
1244 sql << " LIMIT " << options.limit << " OFFSET " << options.offset;
1245
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) {
1249 return result;
1250 }
1251
1252 while (sqlite3_step(stmt) == SQLITE_ROW) {
1253 result.push_back(parse_row(stmt));
1254 }
1255
1256 sqlite3_finalize(stmt);
1257 return result;
1258}
1259
1260std::vector<client::routing_rule> routing_repository::find_enabled_rules() const {
1262 options.enabled_only = true;
1263 options.order_by_priority = true;
1264 return find_rules(options);
1265}
1266
1267VoidResult routing_repository::remove(std::string_view rule_id) {
1268 if (!db_) {
1269 return VoidResult(kcenon::common::error_info{
1270 -1, "Database not initialized", "routing_repository"});
1271 }
1272
1273 static constexpr const char* sql = "DELETE FROM routing_rules WHERE rule_id = ?";
1274
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"});
1280 }
1281
1282 sqlite3_bind_text(stmt, 1, rule_id.data(), static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1283
1284 auto rc = sqlite3_step(stmt);
1285 sqlite3_finalize(stmt);
1286
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"});
1291 }
1292
1293 return kcenon::common::ok();
1294}
1295
1296bool routing_repository::exists(std::string_view rule_id) const {
1297 if (!db_) return false;
1298
1299 static constexpr const char* sql = "SELECT 1 FROM routing_rules WHERE rule_id = ?";
1300
1301 sqlite3_stmt* stmt = nullptr;
1302 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
1303 return false;
1304 }
1305
1306 sqlite3_bind_text(stmt, 1, rule_id.data(), static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1307
1308 bool found = (sqlite3_step(stmt) == SQLITE_ROW);
1309 sqlite3_finalize(stmt);
1310 return found;
1311}
1312
1313// =============================================================================
1314// Rule Ordering
1315// =============================================================================
1316
1317VoidResult routing_repository::update_priority(std::string_view rule_id, int priority) {
1318 if (!db_) {
1319 return VoidResult(kcenon::common::error_info{
1320 -1, "Database not initialized", "routing_repository"});
1321 }
1322
1323 static constexpr const char* sql = R"(
1324 UPDATE routing_rules SET
1325 priority = ?,
1326 updated_at = CURRENT_TIMESTAMP
1327 WHERE rule_id = ?
1328 )";
1329
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"});
1335 }
1336
1337 sqlite3_bind_int(stmt, 1, priority);
1338 sqlite3_bind_text(stmt, 2, rule_id.data(), static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1339
1340 auto rc = sqlite3_step(stmt);
1341 sqlite3_finalize(stmt);
1342
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"});
1347 }
1348
1349 return kcenon::common::ok();
1350}
1351
1352VoidResult routing_repository::enable_rule(std::string_view rule_id) {
1353 if (!db_) {
1354 return VoidResult(kcenon::common::error_info{
1355 -1, "Database not initialized", "routing_repository"});
1356 }
1357
1358 static constexpr const char* sql = R"(
1359 UPDATE routing_rules SET
1360 enabled = 1,
1361 updated_at = CURRENT_TIMESTAMP
1362 WHERE rule_id = ?
1363 )";
1364
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"});
1370 }
1371
1372 sqlite3_bind_text(stmt, 1, rule_id.data(), static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1373
1374 auto rc = sqlite3_step(stmt);
1375 sqlite3_finalize(stmt);
1376
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"});
1381 }
1382
1383 return kcenon::common::ok();
1384}
1385
1386VoidResult routing_repository::disable_rule(std::string_view rule_id) {
1387 if (!db_) {
1388 return VoidResult(kcenon::common::error_info{
1389 -1, "Database not initialized", "routing_repository"});
1390 }
1391
1392 static constexpr const char* sql = R"(
1393 UPDATE routing_rules SET
1394 enabled = 0,
1395 updated_at = CURRENT_TIMESTAMP
1396 WHERE rule_id = ?
1397 )";
1398
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"});
1404 }
1405
1406 sqlite3_bind_text(stmt, 1, rule_id.data(), static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1407
1408 auto rc = sqlite3_step(stmt);
1409 sqlite3_finalize(stmt);
1410
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"});
1415 }
1416
1417 return kcenon::common::ok();
1418}
1419
1420// =============================================================================
1421// Statistics
1422// =============================================================================
1423
1424VoidResult routing_repository::increment_triggered(std::string_view rule_id) {
1425 if (!db_) {
1426 return VoidResult(kcenon::common::error_info{
1427 -1, "Database not initialized", "routing_repository"});
1428 }
1429
1430 static constexpr const char* sql = R"(
1431 UPDATE routing_rules SET
1432 triggered_count = triggered_count + 1,
1433 last_triggered = CURRENT_TIMESTAMP
1434 WHERE rule_id = ?
1435 )";
1436
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"});
1442 }
1443
1444 sqlite3_bind_text(stmt, 1, rule_id.data(), static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1445
1446 auto rc = sqlite3_step(stmt);
1447 sqlite3_finalize(stmt);
1448
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"});
1453 }
1454
1455 return kcenon::common::ok();
1456}
1457
1458VoidResult routing_repository::increment_success(std::string_view rule_id) {
1459 if (!db_) {
1460 return VoidResult(kcenon::common::error_info{
1461 -1, "Database not initialized", "routing_repository"});
1462 }
1463
1464 static constexpr const char* sql = R"(
1465 UPDATE routing_rules SET success_count = success_count + 1 WHERE rule_id = ?
1466 )";
1467
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"});
1473 }
1474
1475 sqlite3_bind_text(stmt, 1, rule_id.data(), static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1476
1477 auto rc = sqlite3_step(stmt);
1478 sqlite3_finalize(stmt);
1479
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"});
1484 }
1485
1486 return kcenon::common::ok();
1487}
1488
1489VoidResult routing_repository::increment_failure(std::string_view rule_id) {
1490 if (!db_) {
1491 return VoidResult(kcenon::common::error_info{
1492 -1, "Database not initialized", "routing_repository"});
1493 }
1494
1495 static constexpr const char* sql = R"(
1496 UPDATE routing_rules SET failure_count = failure_count + 1 WHERE rule_id = ?
1497 )";
1498
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"});
1504 }
1505
1506 sqlite3_bind_text(stmt, 1, rule_id.data(), static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1507
1508 auto rc = sqlite3_step(stmt);
1509 sqlite3_finalize(stmt);
1510
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"});
1515 }
1516
1517 return kcenon::common::ok();
1518}
1519
1520VoidResult routing_repository::reset_statistics(std::string_view rule_id) {
1521 if (!db_) {
1522 return VoidResult(kcenon::common::error_info{
1523 -1, "Database not initialized", "routing_repository"});
1524 }
1525
1526 static constexpr const char* sql = R"(
1527 UPDATE routing_rules SET
1528 triggered_count = 0,
1529 success_count = 0,
1530 failure_count = 0,
1531 last_triggered = NULL
1532 WHERE rule_id = ?
1533 )";
1534
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"});
1540 }
1541
1542 sqlite3_bind_text(stmt, 1, rule_id.data(), static_cast<int>(rule_id.size()), SQLITE_TRANSIENT);
1543
1544 auto rc = sqlite3_step(stmt);
1545 sqlite3_finalize(stmt);
1546
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"});
1551 }
1552
1553 return kcenon::common::ok();
1554}
1555
1556size_t routing_repository::count() const {
1557 if (!db_) return 0;
1558
1559 static constexpr const char* sql = "SELECT COUNT(*) FROM routing_rules";
1560
1561 sqlite3_stmt* stmt = nullptr;
1562 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
1563 return 0;
1564 }
1565
1566 size_t result = 0;
1567 if (sqlite3_step(stmt) == SQLITE_ROW) {
1568 result = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1569 }
1570
1571 sqlite3_finalize(stmt);
1572 return result;
1573}
1574
1575size_t routing_repository::count_enabled() const {
1576 if (!db_) return 0;
1577
1578 static constexpr const char* sql = "SELECT COUNT(*) FROM routing_rules WHERE enabled = 1";
1579
1580 sqlite3_stmt* stmt = nullptr;
1581 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
1582 return 0;
1583 }
1584
1585 size_t result = 0;
1586 if (sqlite3_step(stmt) == SQLITE_ROW) {
1587 result = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1588 }
1589
1590 sqlite3_finalize(stmt);
1591 return result;
1592}
1593
1594// =============================================================================
1595// Database Information
1596// =============================================================================
1597
1598bool routing_repository::is_valid() const noexcept {
1599 return db_ != nullptr;
1600}
1601
1602// =============================================================================
1603// Private Implementation
1604// =============================================================================
1605
1607 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
1609
1610 int col = 0;
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++);
1614 rule.description = get_text_column(stmt, col++);
1615 rule.enabled = (get_int_column(stmt, col++) != 0);
1616 rule.priority = get_int_column(stmt, col++);
1617
1618 auto conditions_json = get_text_column(stmt, col++);
1619 rule.conditions = deserialize_conditions(conditions_json);
1620
1621 auto actions_json = get_text_column(stmt, col++);
1622 rule.actions = deserialize_actions(actions_json);
1623
1624 rule.schedule_cron = get_optional_text(stmt, col++);
1625
1626 auto effective_from_str = get_text_column(stmt, col++);
1627 rule.effective_from = from_optional_timestamp(effective_from_str.c_str());
1628
1629 auto effective_until_str = get_text_column(stmt, col++);
1630 rule.effective_until = from_optional_timestamp(effective_until_str.c_str());
1631
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++));
1635
1636 auto last_triggered_str = get_text_column(stmt, col++);
1637 rule.last_triggered = from_timestamp_string(last_triggered_str.c_str());
1638
1639 auto created_str = get_text_column(stmt, col++);
1640 rule.created_at = from_timestamp_string(created_str.c_str());
1641
1642 auto updated_str = get_text_column(stmt, col++);
1643 rule.updated_at = from_timestamp_string(updated_str.c_str());
1644
1645 return rule;
1646}
1647
1648} // namespace kcenon::pacs::storage
1649
1650#endif // PACS_WITH_DATABASE_SYSTEM
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
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 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 >
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.
Definition job_types.h:54
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.
Definition job_types.h:181
@ 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.