Compare commits

...

29 Commits

Author SHA1 Message Date
Priec
8ec1fa1761 validation2 I dont know if its correct??? 2025-10-26 16:44:15 +01:00
Priec
11185282c4 cargo fix on server 2025-10-26 16:07:19 +01:00
Priec
492f1f1e55 with last commit, we can simplify the logic + remove old 2025_ prefix for search of the tables 2025-10-17 22:55:59 +02:00
Priec
241ab99584 get column gets converted to get column with index automatically now 2025-10-17 22:44:36 +02:00
Priec
8bd5b5c62f marked crucial todo for 2025_ deprecated prefixing 2025-09-21 21:47:32 +02:00
Priec
7e21258d2e all tests passed without any problem 2025-09-21 20:50:14 +02:00
Priec
49277cfdd4 last error remaining 2025-09-18 18:53:55 +02:00
Priec
1f6dc3cd75 failed tests all over the place now 2025-09-18 10:47:27 +02:00
Priec
7350b0985c JSONB on table scripts also now 2025-09-18 10:47:01 +02:00
Priec
73bc6dc99c fixing tests and migration to the serialized deserialized JSONB2 2025-09-17 21:19:41 +02:00
Priec
095645a209 fixing tests and migration to the serialized deserialized JSONB 2025-09-17 21:10:11 +02:00
Priec
532977056d fixing serialization and deserialization in the data insertion 2025-09-17 09:38:05 +02:00
Priec
2435f58256 table definition now has serialization deserialization properly implemented 2025-09-16 22:55:49 +02:00
Priec
ceb560c658 serialization of the gRPC JSONB now fully works for the validation 2025-09-14 11:24:27 +02:00
Priec
d88c239bf6 serde of jsonb in grpc 2025-09-14 10:56:38 +02:00
filipriec
01c4ff2e14 validation backend 2025-09-13 21:15:44 +02:00
Priec
c2890e1f3d tests via script and make test works now 2025-09-13 08:53:26 +02:00
Priec
e1ea44c68c inputing data for the client for the validation 2025-09-12 23:06:42 +02:00
Priec
cec2361b00 validation1 for the form 2025-09-12 21:25:49 +02:00
Priec
9672b9949c finally a working space 2025-09-12 19:14:21 +02:00
Priec
e4e9594a9d minor changes 2025-09-12 19:05:17 +02:00
Priec
6daa5202b1 debug is now running properly in the background without any issues 2025-09-12 18:17:52 +02:00
Priec
cae47da5f2 reused grpc connections, not a constant refreshes anymore, all fixed now, keep on fixing other bugs 2025-09-12 18:15:46 +02:00
filipriec
85c7c89c28 space2 is now debugging better 2025-09-12 15:46:14 +02:00
Priec
0d80266e9b space commands here we go again 2025-09-11 22:36:40 +02:00
Priec
a604d62d44 inputs from keyboard are now decoupled 2025-09-10 22:12:22 +02:00
Priec
2cbbfd21aa revert works on login, now do the same for other pages as well 2025-09-08 22:11:53 +02:00
Priec
1c17d07497 space and revert working properly well, also shift 2025-09-08 20:05:39 +02:00
Priec
ad15becd7a doing key sequencing via space 2025-09-08 12:56:03 +02:00
81 changed files with 2662 additions and 4971 deletions

125
Cargo.lock generated
View File

@@ -595,8 +595,8 @@ dependencies = [
"dotenvy", "dotenvy",
"futures", "futures",
"lazy_static", "lazy_static",
"prost", "prost 0.13.5",
"prost-types", "prost-types 0.13.5",
"ratatui", "ratatui",
"rstest", "rstest",
"serde", "serde",
@@ -637,9 +637,11 @@ dependencies = [
name = "common" name = "common"
version = "0.5.0" version = "0.5.0"
dependencies = [ dependencies = [
"prost", "prost 0.13.5",
"prost-types", "prost-build 0.14.1",
"prost-types 0.13.5",
"serde", "serde",
"serde_json",
"tantivy", "tantivy",
"tonic", "tonic",
"tonic-build", "tonic-build",
@@ -1955,6 +1957,15 @@ version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a"
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata 0.1.10",
]
[[package]] [[package]]
name = "matchit" name = "matchit"
version = "0.8.4" version = "0.8.4"
@@ -2495,7 +2506,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
dependencies = [ dependencies = [
"bytes", "bytes",
"prost-derive", "prost-derive 0.13.5",
]
[[package]]
name = "prost"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d"
dependencies = [
"bytes",
"prost-derive 0.14.1",
] ]
[[package]] [[package]]
@@ -2511,8 +2532,28 @@ dependencies = [
"once_cell", "once_cell",
"petgraph", "petgraph",
"prettyplease", "prettyplease",
"prost", "prost 0.13.5",
"prost-types", "prost-types 0.13.5",
"regex",
"syn 2.0.104",
"tempfile",
]
[[package]]
name = "prost-build"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1"
dependencies = [
"heck",
"itertools 0.14.0",
"log",
"multimap",
"once_cell",
"petgraph",
"prettyplease",
"prost 0.14.1",
"prost-types 0.14.1",
"regex", "regex",
"syn 2.0.104", "syn 2.0.104",
"tempfile", "tempfile",
@@ -2531,13 +2572,35 @@ dependencies = [
"syn 2.0.104", "syn 2.0.104",
] ]
[[package]]
name = "prost-derive"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425"
dependencies = [
"anyhow",
"itertools 0.14.0",
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]] [[package]]
name = "prost-types" name = "prost-types"
version = "0.13.5" version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
dependencies = [ dependencies = [
"prost", "prost 0.13.5",
]
[[package]]
name = "prost-types"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72"
dependencies = [
"prost 0.14.1",
] ]
[[package]] [[package]]
@@ -2747,8 +2810,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-automata", "regex-automata 0.4.9",
"regex-syntax", "regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
] ]
[[package]] [[package]]
@@ -2759,9 +2831,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-syntax", "regex-syntax 0.8.5",
] ]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.5" version = "0.8.5"
@@ -3026,7 +3104,7 @@ version = "0.5.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"common", "common",
"prost", "prost 0.13.5",
"serde", "serde",
"serde_json", "serde_json",
"sqlx", "sqlx",
@@ -3132,8 +3210,9 @@ dependencies = [
"futures", "futures",
"jsonwebtoken", "jsonwebtoken",
"lazy_static", "lazy_static",
"prost", "prost 0.13.5",
"prost-types", "prost-build 0.14.1",
"prost-types 0.13.5",
"rand 0.9.2", "rand 0.9.2",
"regex", "regex",
"rstest", "rstest",
@@ -3751,7 +3830,7 @@ dependencies = [
"fnv", "fnv",
"once_cell", "once_cell",
"plist", "plist",
"regex-syntax", "regex-syntax 0.8.5",
"serde", "serde",
"serde_derive", "serde_derive",
"serde_json", "serde_json",
@@ -3857,7 +3936,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18" checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"regex-syntax", "regex-syntax 0.8.5",
"utf8-ranges", "utf8-ranges",
] ]
@@ -4169,7 +4248,7 @@ dependencies = [
"hyper-util", "hyper-util",
"percent-encoding", "percent-encoding",
"pin-project", "pin-project",
"prost", "prost 0.13.5",
"socket2 0.5.10", "socket2 0.5.10",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
@@ -4187,8 +4266,8 @@ checksum = "eac6f67be712d12f0b41328db3137e0d0757645d8904b4cb7d51cd9c2279e847"
dependencies = [ dependencies = [
"prettyplease", "prettyplease",
"proc-macro2", "proc-macro2",
"prost-build", "prost-build 0.13.5",
"prost-types", "prost-types 0.13.5",
"quote", "quote",
"syn 2.0.104", "syn 2.0.104",
] ]
@@ -4199,8 +4278,8 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9687bd5bfeafebdded2356950f278bba8226f0b32109537c4253406e09aafe1" checksum = "f9687bd5bfeafebdded2356950f278bba8226f0b32109537c4253406e09aafe1"
dependencies = [ dependencies = [
"prost", "prost 0.13.5",
"prost-types", "prost-types 0.13.5",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tonic", "tonic",
@@ -4287,10 +4366,14 @@ version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [ dependencies = [
"matchers",
"nu-ansi-term", "nu-ansi-term",
"once_cell",
"regex",
"sharded-slab", "sharded-slab",
"smallvec", "smallvec",
"thread_local", "thread_local",
"tracing",
"tracing-core", "tracing-core",
"tracing-log", "tracing-log",
] ]

1
client/.gitignore vendored
View File

@@ -1 +1,2 @@
canvas_config.toml.txt canvas_config.toml.txt
ui_debug.log

View File

@@ -8,7 +8,7 @@ license.workspace = true
anyhow = { workspace = true } anyhow = { workspace = true }
async-trait = "0.1.88" async-trait = "0.1.88"
common = { path = "../common" } common = { path = "../common" }
canvas = { path = "../canvas", features = ["gui", "suggestions", "cursor-style", "keymap"] } canvas = { path = "../canvas", features = ["gui", "suggestions", "cursor-style", "keymap", "validation"] }
ratatui = { workspace = true } ratatui = { workspace = true }
crossterm = { workspace = true } crossterm = { workspace = true }
@@ -24,14 +24,15 @@ tokio = { version = "1.44.2", features = ["full", "macros"] }
toml = { workspace = true } toml = { workspace = true }
tonic = "0.13.0" tonic = "0.13.0"
tracing = "0.1.41" tracing = "0.1.41"
tracing-subscriber = "0.3.19" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tui-textarea = { version = "0.7.0", features = ["crossterm", "ratatui", "search"] } tui-textarea = { version = "0.7.0", features = ["crossterm", "ratatui", "search"] }
unicode-segmentation = "1.12.0" unicode-segmentation = "1.12.0"
unicode-width.workspace = true unicode-width.workspace = true
[features] [features]
default = [] default = ["validation"]
ui-debug = [] ui-debug = []
validation = []
[dev-dependencies] [dev-dependencies]
rstest = "0.25.0" rstest = "0.25.0"

View File

@@ -5,6 +5,7 @@ enter_command_mode = [":", "ctrl+;"]
next_buffer = ["space+b+n"] next_buffer = ["space+b+n"]
previous_buffer = ["space+b+p"] previous_buffer = ["space+b+p"]
close_buffer = ["space+b+d"] close_buffer = ["space+b+d"]
revert = ["space+b+r"]
[keybindings.general] [keybindings.general]
up = ["k", "Up"] up = ["k", "Up"]
@@ -27,7 +28,6 @@ move_up = ["Up"]
move_down = ["Down"] move_down = ["Down"]
toggle_sidebar = ["ctrl+t"] toggle_sidebar = ["ctrl+t"]
toggle_buffer_list = ["ctrl+b"] toggle_buffer_list = ["ctrl+b"]
revert = ["space+b+r"]
# MODE SPECIFIC # MODE SPECIFIC
# READ ONLY MODE # READ ONLY MODE
@@ -60,7 +60,7 @@ prev_field = ["Shift+Tab"]
[keybindings.highlight] [keybindings.highlight]
exit_highlight_mode = ["esc"] exit_highlight_mode = ["esc"]
enter_highlight_mode_linewise = ["ctrl+v"] enter_highlight_mode_linewise = ["shift+v"]
### AUTOGENERATED CANVAS CONFIG ### AUTOGENERATED CANVAS CONFIG
# Required # Required

View File

@@ -250,28 +250,63 @@ impl Config {
key: KeyCode, key: KeyCode,
modifiers: KeyModifiers, modifiers: KeyModifiers,
) -> bool { ) -> bool {
// Special handling for shift+character combinations
if binding.to_lowercase().starts_with("shift+") { // Normalize binding once
let parts: Vec<&str> = binding.split('+').collect(); let binding_lc = binding.to_lowercase();
if parts.len() == 2 && parts[1].len() == 1 {
let expected_lowercase = parts[1].chars().next().unwrap().to_lowercase().next().unwrap(); // Robust handling for Shift+Tab
let expected_uppercase = expected_lowercase.to_uppercase().next().unwrap(); // Accept either BackTab (with or without SHIFT flagged) or Tab+SHIFT
if let KeyCode::Char(actual_char) = key { if binding_lc == "shift+tab" || binding_lc == "backtab" {
if actual_char == expected_uppercase && modifiers.contains(KeyModifiers::SHIFT) { return match key {
return true; KeyCode::BackTab => true,
} KeyCode::Tab => modifiers.contains(KeyModifiers::SHIFT),
_ => false,
};
} }
// If binding contains '+', distinguish between:
// - modifier combos (e.g., ctrl+shift+s) => single key + modifiers
// - multi-key sequences (e.g., space+b+r, g+g) => NOT a single key
if binding_lc.contains('+') {
let parts: Vec<&str> = binding_lc.split('+').collect();
let is_modifier = |t: &str| {
matches!(
t,
"ctrl" | "control" | "shift" | "alt" | "super" | "windows" | "cmd" | "hyper" | "meta"
)
};
let non_modifier_count = parts.iter().filter(|p| !is_modifier(p)).count();
if non_modifier_count > 1 {
// This is a multi-key sequence (e.g., space+b+r, g+g), not a single keybind.
// It must be handled by the sequence engine, not here.
return false;
} }
} }
// Handle Shift+Tab -> BackTab // Robust handling for shift+<char> (letters)
if binding.to_lowercase() == "shift+tab" && key == KeyCode::BackTab && modifiers.is_empty() { // Many terminals send uppercase Char without SHIFT bit.
if binding_lc.starts_with("shift+") {
let parts: Vec<&str> = binding.split('+').collect();
if parts.len() == 2 && parts[1].chars().count() == 1 {
let base = parts[1].chars().next().unwrap();
let upper = base.to_ascii_uppercase();
let lower = base.to_ascii_lowercase();
if let KeyCode::Char(actual) = key {
// Accept uppercase char regardless of SHIFT bit
if actual == upper {
return true; return true;
} }
// Also accept lowercase char with SHIFT flagged (some terms do this)
if actual == lower && modifiers.contains(KeyModifiers::SHIFT) {
return true;
}
}
}
}
// Handle multi-character bindings (all standard keys without modifiers) // Handle multi-character bindings (all standard keys without modifiers)
if binding.len() > 1 && !binding.contains('+') { if binding.len() > 1 && !binding.contains('+') {
return match binding.to_lowercase().as_str() { return match binding_lc.as_str() {
// Navigation keys // Navigation keys
"left" => key == KeyCode::Left, "left" => key == KeyCode::Left,
"right" => key == KeyCode::Right, "right" => key == KeyCode::Right,
@@ -371,6 +406,7 @@ impl Config {
let mut expected_key = None; let mut expected_key = None;
for part in parts { for part in parts {
let part_lc = part.to_lowercase();
match part.to_lowercase().as_str() { match part.to_lowercase().as_str() {
// Modifiers // Modifiers
"ctrl" | "control" => expected_modifiers |= KeyModifiers::CONTROL, "ctrl" | "control" => expected_modifiers |= KeyModifiers::CONTROL,
@@ -789,12 +825,43 @@ impl Config {
None None
} }
// Normalize bindings for canvas consumption:
// - "shift+<char>" -> also add "<CHAR>"
// - "shift+tab" -> also add "backtab"
// This keeps your config human-friendly while making the canvas happy.
fn normalize_for_canvas(
map: &HashMap<String, Vec<String>>,
) -> HashMap<String, Vec<String>> {
let mut out: HashMap<String, Vec<String>> = HashMap::new();
for (action, bindings) in map {
let mut new_list: Vec<String> = Vec::new();
for b in bindings {
new_list.push(b.clone());
let blc = b.to_lowercase();
if blc.starts_with("shift+") {
let parts: Vec<&str> = b.split('+').collect();
if parts.len() == 2 && parts[1].chars().count() == 1 {
let ch = parts[1].chars().next().unwrap();
new_list.push(ch.to_ascii_uppercase().to_string());
}
if blc == "shift+tab" {
new_list.push("backtab".to_string());
}
}
if blc == "shift+tab" {
new_list.push("backtab".to_string());
}
}
out.insert(action.clone(), new_list);
}
out
}
pub fn build_canvas_keymap(&self) -> CanvasKeyMap { pub fn build_canvas_keymap(&self) -> CanvasKeyMap {
CanvasKeyMap::from_mode_maps( let ro = Self::normalize_for_canvas(&self.keybindings.read_only);
&self.keybindings.read_only, let ed = Self::normalize_for_canvas(&self.keybindings.edit);
&self.keybindings.edit, let hl = Self::normalize_for_canvas(&self.keybindings.highlight);
&self.keybindings.highlight, CanvasKeyMap::from_mode_maps(&ro, &ed, &hl)
)
} }
} }

View File

@@ -1,6 +1,7 @@
// client/src/config/key_sequences.rs // client/src/config/key_sequences.rs
use crossterm::event::{KeyCode, KeyModifiers}; use crossterm::event::{KeyCode, KeyModifiers};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tracing::info;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct ParsedKey { pub struct ParsedKey {
@@ -25,19 +26,21 @@ impl KeySequenceTracker {
} }
pub fn reset(&mut self) { pub fn reset(&mut self) {
info!("KeySequenceTracker.reset() from {:?}", self.current_sequence);
self.current_sequence.clear(); self.current_sequence.clear();
self.last_key_time = Instant::now(); self.last_key_time = Instant::now();
} }
pub fn add_key(&mut self, key: KeyCode) -> bool { pub fn add_key(&mut self, key: KeyCode) -> bool {
// Check if timeout has expired
let now = Instant::now(); let now = Instant::now();
if now.duration_since(self.last_key_time) > self.timeout { if now.duration_since(self.last_key_time) > self.timeout {
info!("KeySequenceTracker timeout — reset before adding {:?}", key);
self.reset(); self.reset();
} }
self.current_sequence.push(key); self.current_sequence.push(key);
self.last_key_time = now; self.last_key_time = now;
info!("KeySequenceTracker state after add: {:?}", self.current_sequence);
true true
} }
@@ -67,6 +70,7 @@ impl KeySequenceTracker {
// Helper function to convert any KeyCode to a string representation // Helper function to convert any KeyCode to a string representation
pub fn key_to_string(key: &KeyCode) -> String { pub fn key_to_string(key: &KeyCode) -> String {
match key { match key {
KeyCode::Char(' ') => "space".to_string(),
KeyCode::Char(c) => c.to_string(), KeyCode::Char(c) => c.to_string(),
KeyCode::Left => "left".to_string(), KeyCode::Left => "left".to_string(),
KeyCode::Right => "right".to_string(), KeyCode::Right => "right".to_string(),
@@ -90,6 +94,7 @@ pub fn key_to_string(key: &KeyCode) -> String {
// Helper function to convert a string to a KeyCode // Helper function to convert a string to a KeyCode
pub fn string_to_keycode(s: &str) -> Option<KeyCode> { pub fn string_to_keycode(s: &str) -> Option<KeyCode> {
match s.to_lowercase().as_str() { match s.to_lowercase().as_str() {
"space" => Some(KeyCode::Char(' ')),
"left" => Some(KeyCode::Left), "left" => Some(KeyCode::Left),
"right" => Some(KeyCode::Right), "right" => Some(KeyCode::Right),
"up" => Some(KeyCode::Up), "up" => Some(KeyCode::Up),
@@ -113,26 +118,21 @@ pub fn string_to_keycode(s: &str) -> Option<KeyCode> {
pub fn parse_binding(binding: &str) -> Vec<ParsedKey> { pub fn parse_binding(binding: &str) -> Vec<ParsedKey> {
let mut sequence = Vec::new(); let mut sequence = Vec::new();
// Handle different binding formats // Split into multi-key sequence:
let parts: Vec<String> = if binding.contains('+') { // - If contains space → sequence split by space
// Format with explicit '+' separators like "g+left" // - Else split by '+'
binding.split('+').map(|s| s.to_string()).collect() let parts: Vec<&str> = if binding.contains(' ') {
} else if binding.contains(' ') { binding.split(' ').collect()
// Format with spaces like "g left"
binding.split(' ').map(|s| s.to_string()).collect()
} else if is_compound_key(binding) {
// A single compound key like "left" or "enter"
vec![binding.to_string()]
} else { } else {
// Simple character sequence like "gg" binding.split('+').collect()
binding.chars().map(|c| c.to_string()).collect()
}; };
for part in &parts { for part in parts {
if let Some(key) = parse_key_part(part) { if let Some(parsed) = parse_key_part(part) {
sequence.push(key); sequence.push(parsed);
} }
} }
sequence sequence
} }
@@ -140,7 +140,7 @@ fn is_compound_key(part: &str) -> bool {
matches!(part.to_lowercase().as_str(), matches!(part.to_lowercase().as_str(),
"esc" | "up" | "down" | "left" | "right" | "enter" | "esc" | "up" | "down" | "left" | "right" | "enter" |
"backspace" | "delete" | "tab" | "backtab" | "home" | "backspace" | "delete" | "tab" | "backtab" | "home" |
"end" | "pageup" | "pagedown" | "insert" "end" | "pageup" | "pagedown" | "insert" | "space"
) )
} }

View File

@@ -0,0 +1,41 @@
// src/input/action.rs
use crate::movement::MovementAction;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BufferAction {
Next,
Previous,
Close,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CoreAction {
Save,
ForceQuit,
SaveAndQuit,
Revert,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AppAction {
// Global/UI
ToggleSidebar,
ToggleBufferList,
OpenSearch,
FindFilePaletteToggle,
// Buffers
Buffer(BufferAction),
// Command mode
EnterCommandMode,
ExitCommandMode,
CommandExecute,
CommandBackspace,
// Navigation across UI
Navigate(MovementAction),
// Core actions
Core(CoreAction),
}

195
client/src/input/engine.rs Normal file
View File

@@ -0,0 +1,195 @@
// src/input/engine.rs
use crate::config::binds::config::Config;
use crate::config::binds::key_sequences::KeySequenceTracker;
use crate::input::action::{AppAction, BufferAction, CoreAction};
use crate::movement::MovementAction;
use crate::modes::handlers::mode_manager::AppMode;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::input::leader::{leader_has_any_start, leader_is_prefix, leader_match_action};
use tracing::info;
#[derive(Debug, Clone, Copy)]
pub struct InputContext {
pub app_mode: AppMode,
pub overlay_active: bool,
pub allow_navigation_capture: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InputOutcome {
Action(AppAction),
Pending, // sequence in progress
PassThrough, // let page/canvas handle it
}
pub struct InputEngine {
seq: KeySequenceTracker,
leader_seq: KeySequenceTracker,
}
impl InputEngine {
pub fn new(normal_timeout_ms: u64, leader_timeout_ms: u64) -> Self {
Self {
seq: KeySequenceTracker::new(normal_timeout_ms),
leader_seq: KeySequenceTracker::new(leader_timeout_ms),
}
}
pub fn reset_sequence(&mut self) {
info!("InputEngine.reset_sequence() leader_seq_before={:?}", self.leader_seq.current_sequence);
self.seq.reset();
self.leader_seq.reset();
}
pub fn has_active_sequence(&self) -> bool {
!self.seq.current_sequence.is_empty()
|| !self.leader_seq.current_sequence.is_empty()
}
pub fn process_key(
&mut self,
key_event: KeyEvent,
ctx: &InputContext,
config: &Config,
) -> InputOutcome {
// Command mode keys are special (exit/execute/backspace) and typed chars
if ctx.app_mode == AppMode::Command {
if config.is_exit_command_mode(key_event.code, key_event.modifiers) {
self.seq.reset();
return InputOutcome::Action(AppAction::ExitCommandMode);
}
if config.is_command_execute(key_event.code, key_event.modifiers) {
self.seq.reset();
return InputOutcome::Action(AppAction::CommandExecute);
}
if config.is_command_backspace(key_event.code, key_event.modifiers) {
self.seq.reset();
return InputOutcome::Action(AppAction::CommandBackspace);
}
// Let command-line collect characters and other keys pass through
self.seq.reset();
return InputOutcome::PassThrough;
}
// If overlays are active, do not intercept (palette, navigation, etc.)
if ctx.overlay_active {
self.seq.reset();
// Also reset leader sequence to avoid leaving a stale "space" active
info!("Overlay active → reset leader_seq (was {:?})", self.leader_seq.current_sequence);
self.leader_seq.reset();
return InputOutcome::PassThrough;
}
// Space-led multi-key sequences (leader = space)
let space = KeyCode::Char(' ');
let leader_active = !self.leader_seq.current_sequence.is_empty()
&& self.leader_seq.current_sequence[0] == space;
// Keep collecting leader sequence even if allow_navigation_capture is false.
if leader_active {
self.leader_seq.add_key(key_event.code);
let sequence = self.leader_seq.get_sequence();
info!(
"Leader active updated: {:?} (added {:?})",
sequence, key_event.code
);
if let Some(action_str) = leader_match_action(config, &sequence) {
info!("Leader matched '{}' with sequence {:?}", action_str, sequence);
if let Some(app_action) = map_action_string(action_str, ctx) {
self.leader_seq.reset();
return InputOutcome::Action(app_action);
}
self.leader_seq.reset();
return InputOutcome::PassThrough;
}
if leader_is_prefix(config, &sequence) {
info!("Leader prefix continuing...");
return InputOutcome::Pending;
}
info!("Leader sequence reset (no match/prefix).");
self.leader_seq.reset();
// fall through to regular handling of this key
} else if ctx.allow_navigation_capture
&& key_event.code == space
&& leader_has_any_start(config)
{
// Start a leader sequence only if capturing is allowed
self.leader_seq.reset();
self.leader_seq.add_key(space);
info!("Leader started: {:?}", self.leader_seq.get_sequence());
return InputOutcome::Pending;
}
// Single-key mapping: try general binds first (arrows, open_search, enter_command_mode)
if let Some(action_str) =
config.get_general_action(key_event.code, key_event.modifiers)
{
if let Some(app_action) = map_action_string(action_str, ctx) {
return InputOutcome::Action(app_action);
}
// Unknown to app layer (likely canvas movement etc.) → pass
return InputOutcome::PassThrough;
}
// Then app-level common/read-only/edit/highlight for UI toggles or core actions
if let Some(action_str) = config.get_app_action(key_event.code, key_event.modifiers) {
if let Some(app_action) = map_action_string(action_str, ctx) {
return InputOutcome::Action(app_action);
}
}
InputOutcome::PassThrough
}
}
fn str_to_movement(s: &str) -> Option<MovementAction> {
match s {
"up" => Some(MovementAction::Up),
"down" => Some(MovementAction::Down),
"left" => Some(MovementAction::Left),
"right" => Some(MovementAction::Right),
"next" => Some(MovementAction::Next),
"previous" => Some(MovementAction::Previous),
"select" => Some(MovementAction::Select),
"esc" => Some(MovementAction::Esc),
_ => None,
}
}
fn map_action_string(action: &str, ctx: &InputContext) -> Option<AppAction> {
match action {
// Global/UI
"toggle_sidebar" => Some(AppAction::ToggleSidebar),
"toggle_buffer_list" => Some(AppAction::ToggleBufferList),
"open_search" => Some(AppAction::OpenSearch),
"find_file_palette_toggle" => Some(AppAction::FindFilePaletteToggle),
// Buffers
"next_buffer" => Some(AppAction::Buffer(BufferAction::Next)),
"previous_buffer" => Some(AppAction::Buffer(BufferAction::Previous)),
"close_buffer" => Some(AppAction::Buffer(BufferAction::Close)),
// Command mode
"enter_command_mode" => Some(AppAction::EnterCommandMode),
"exit_command_mode" => Some(AppAction::ExitCommandMode),
"command_execute" => Some(AppAction::CommandExecute),
"command_backspace" => Some(AppAction::CommandBackspace),
// Navigation across UI (only if allowed)
s if str_to_movement(s).is_some() && ctx.allow_navigation_capture => {
Some(AppAction::Navigate(str_to_movement(s).unwrap()))
}
// Core actions
"save" => Some(AppAction::Core(CoreAction::Save)),
"force_quit" => Some(AppAction::Core(CoreAction::ForceQuit)),
"save_and_quit" => Some(AppAction::Core(CoreAction::SaveAndQuit)),
"revert" => Some(AppAction::Core(CoreAction::Revert)),
// Unknown to app layer: ignore (canvas-specific actions, etc.)
_ => None,
}
}

View File

@@ -0,0 +1,74 @@
// src/input/leader.rs
use crate::config::binds::config::Config;
use crate::config::binds::key_sequences::parse_binding;
use crossterm::event::KeyCode;
/// Collect leader (= space-prefixed) bindings from *all* binding maps
fn leader_bindings<'a>(config: &'a Config) -> Vec<(&'a str, Vec<KeyCode>)> {
let mut out = Vec::new();
// Include all keybinding maps, not just global
let all_modes: Vec<&std::collections::HashMap<String, Vec<String>>> = vec![
&config.keybindings.general,
&config.keybindings.read_only,
&config.keybindings.edit,
&config.keybindings.highlight,
&config.keybindings.command,
&config.keybindings.common,
&config.keybindings.global,
];
for mode in all_modes {
for (action, bindings) in mode {
for b in bindings {
let parsed = parse_binding(b);
if parsed.first().map(|pk| pk.code) == Some(KeyCode::Char(' ')) {
let codes =
parsed.into_iter().map(|pk| pk.code).collect::<Vec<_>>();
out.push((action.as_str(), codes));
}
}
}
}
out
}
/// Is there any leader binding configured at all?
pub fn leader_has_any_start(config: &Config) -> bool {
leader_bindings(config)
.iter()
.any(|(_, seq)| seq.first() == Some(&KeyCode::Char(' ')))
}
/// Is `sequence` a prefix of any configured leader sequence?
pub fn leader_is_prefix(config: &Config, sequence: &[KeyCode]) -> bool {
if sequence.is_empty() || sequence[0] != KeyCode::Char(' ') {
return false;
}
for (_, full) in leader_bindings(config) {
if full.len() > sequence.len()
&& full.iter().zip(sequence.iter()).all(|(a, b)| a == b)
{
return true;
}
}
false
}
/// Is `sequence` an exact leader match? If yes, return the action string.
pub fn leader_match_action<'a>(
config: &'a Config,
sequence: &[KeyCode],
) -> Option<&'a str> {
if sequence.is_empty() || sequence[0] != KeyCode::Char(' ') {
return None;
}
for (action, full) in leader_bindings(config) {
if full.len() == sequence.len()
&& full.iter().zip(sequence.iter()).all(|(a, b)| a == b)
{
return Some(action);
}
}
None
}

4
client/src/input/mod.rs Normal file
View File

@@ -0,0 +1,4 @@
// src/input/mod.rs
pub mod action;
pub mod engine;
pub mod leader;

View File

@@ -14,6 +14,7 @@ pub mod search;
pub mod bottom_panel; pub mod bottom_panel;
pub mod pages; pub mod pages;
pub mod movement; pub mod movement;
pub mod input;
pub use ui::run_ui; pub use ui::run_ui;

View File

@@ -4,26 +4,35 @@ use client::run_ui;
use client::utils::debug_logger::UiDebugWriter; use client::utils::debug_logger::UiDebugWriter;
use dotenvy::dotenv; use dotenvy::dotenv;
use anyhow::Result; use anyhow::Result;
use tracing_subscriber; use tracing_subscriber::EnvFilter;
use std::env; use std::env;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
#[cfg(feature = "ui-debug")] #[cfg(feature = "ui-debug")]
{ {
// If ui-debug is on, set up our custom writer. use std::sync::Once;
static INIT_LOGGER: Once = Once::new();
INIT_LOGGER.call_once(|| {
let writer = UiDebugWriter::new(); let writer = UiDebugWriter::new();
tracing_subscriber::fmt() let _ = tracing_subscriber::fmt()
.with_level(false) // Don't show INFO, ERROR, etc. .with_max_level(tracing::Level::DEBUG)
.with_target(false) // Don't show the module path. .with_target(false)
.without_time() // This is the correct and simpler method. .without_time()
.with_writer(move || writer.clone()) .with_writer(move || writer.clone())
.init(); // Filter out noisy grpc/h2 internals
.with_env_filter("client=debug,tonic=info,h2=info,tower=info")
.try_init();
client::utils::debug_logger::spawn_file_logger();
});
} }
#[cfg(not(feature = "ui-debug"))] #[cfg(not(feature = "ui-debug"))]
{ {
if env::var("ENABLE_TRACING").is_ok() { if env::var("ENABLE_TRACING").is_ok() {
tracing_subscriber::fmt::init(); let _ = tracing_subscriber::fmt::try_init();
} }
} }

View File

@@ -1,6 +1,7 @@
// src/modes/handlers/event.rs // src/modes/handlers/event.rs
use crate::config::binds::config::Config; use crate::config::binds::config::Config;
use crate::config::binds::key_sequences::KeySequenceTracker; use crate::input::engine::{InputContext, InputEngine, InputOutcome};
use crate::input::action::{AppAction, BufferAction, CoreAction};
use crate::buffer::{AppView, BufferState, switch_buffer, toggle_buffer_list}; use crate::buffer::{AppView, BufferState, switch_buffer, toggle_buffer_list};
use crate::sidebar::toggle_sidebar; use crate::sidebar::toggle_sidebar;
use crate::search::event::handle_search_palette_event; use crate::search::event::handle_search_palette_event;
@@ -51,6 +52,7 @@ use common::proto::komp_ac::search::search_response::Hit;
use crossterm::event::{Event, KeyCode}; use crossterm::event::{Event, KeyCode};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::sync::mpsc::unbounded_channel; use tokio::sync::mpsc::unbounded_channel;
use tracing::info;
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum EventOutcome { pub enum EventOutcome {
@@ -76,7 +78,7 @@ pub struct EventHandler {
pub command_message: String, pub command_message: String,
pub edit_mode_cooldown: bool, pub edit_mode_cooldown: bool,
pub ideal_cursor_column: usize, pub ideal_cursor_column: usize,
pub key_sequence_tracker: KeySequenceTracker, pub input_engine: InputEngine,
pub auth_client: AuthClient, pub auth_client: AuthClient,
pub grpc_client: GrpcClient, pub grpc_client: GrpcClient,
pub login_result_sender: mpsc::Sender<LoginResult>, pub login_result_sender: mpsc::Sender<LoginResult>,
@@ -106,8 +108,10 @@ impl EventHandler {
command_message: String::new(), command_message: String::new(),
edit_mode_cooldown: false, edit_mode_cooldown: false,
ideal_cursor_column: 0, ideal_cursor_column: 0,
key_sequence_tracker: KeySequenceTracker::new(400), input_engine: InputEngine::new(400, 5000),
auth_client: AuthClient::new().await?, auth_client: AuthClient::with_channel(
grpc_client.channel()
).await?,
grpc_client, grpc_client,
login_result_sender, login_result_sender,
register_result_sender, register_result_sender,
@@ -226,6 +230,12 @@ impl EventHandler {
) -> Result<EventOutcome> { ) -> Result<EventOutcome> {
if app_state.ui.show_search_palette { if app_state.ui.show_search_palette {
if let Event::Key(key_event) = event { if let Event::Key(key_event) = event {
info!(
"RAW KEY: code={:?} mods={:?} active_seq={} ",
key_event.code,
key_event.modifiers,
self.input_engine.has_active_sequence(),
);
if let Some(message) = handle_search_palette_event( if let Some(message) = handle_search_palette_event(
key_event, key_event,
app_state, app_state,
@@ -296,17 +306,79 @@ impl EventHandler {
if let Event::Key(key_event) = event { if let Event::Key(key_event) = event {
let key_code = key_event.code; let key_code = key_event.code;
let modifiers = key_event.modifiers; let modifiers = key_event.modifiers;
info!(
"RAW KEY: code={:?} mods={:?} pre_active_seq={}",
key_code, modifiers, self.input_engine.has_active_sequence()
);
// LOGIN: canvas <-> buttons focus handoff
// Do not let Login canvas receive keys when overlays/palettes are active
let overlay_active = self.command_mode let overlay_active = self.command_mode
|| app_state.ui.show_search_palette || app_state.ui.show_search_palette
|| self.navigation_state.active; || self.navigation_state.active;
// Determine if canvas is in edit mode (we avoid capturing navigation then)
let in_form_edit_mode = matches!(
&router.current,
Page::Form(path) if {
if let Some(editor) = app_state.editor_for_path_ref(path) {
editor.mode() == CanvasMode::Edit
} else { false }
}
);
// Centralized key -> action resolution
let allow_nav = self.input_engine.has_active_sequence()
|| (!in_form_edit_mode && !overlay_active);
let input_ctx = InputContext {
app_mode: current_mode,
overlay_active,
allow_navigation_capture: allow_nav,
};
info!(
"InputContext: app_mode={:?}, overlay_active={}, in_form_edit_mode={}, allow_nav={}, has_active_seq={}",
current_mode, overlay_active, in_form_edit_mode, allow_nav, self.input_engine.has_active_sequence()
);
let outcome = self.input_engine.process_key(key_event, &input_ctx, config);
info!(
"ENGINE OUTCOME: {:?} post_active_seq={}",
outcome, self.input_engine.has_active_sequence()
);
match outcome {
InputOutcome::Action(action) => {
if let Some(outcome) = self
.handle_app_action(
action,
key_event, // pass original key
config,
terminal,
command_handler,
auth_state,
buffer_state,
app_state,
router,
)
.await?
{
return Ok(outcome);
}
// No early return on None (e.g., Navigate) — fall through
}
InputOutcome::Pending => {
// waiting for more keys in a sequence
return Ok(EventOutcome::Ok(String::new()));
}
InputOutcome::PassThrough => {
// fall through to page/canvas handlers
}
}
// LOGIN: canvas <-> buttons focus handoff
// Do not let Login canvas receive keys when overlays/palettes are active
if !overlay_active { if !overlay_active {
if let Page::Login(login_page) = &mut router.current { if let Page::Login(login_page) = &mut router.current {
let outcome = let outcome =
login::event::handle_login_event(event, app_state, login_page)?; login::event::handle_login_event(event.clone(), app_state, login_page)?;
// Only return if the login page actually consumed the key // Only return if the login page actually consumed the key
if !outcome.get_message_if_ok().is_empty() { if !outcome.get_message_if_ok().is_empty() {
return Ok(outcome); return Ok(outcome);
@@ -321,17 +393,30 @@ impl EventHandler {
if !outcome.get_message_if_ok().is_empty() { if !outcome.get_message_if_ok().is_empty() {
return Ok(outcome); return Ok(outcome);
} }
} else if let Page::Form(path) = &router.current { } else if let Page::Form(path_str) = &router.current {
let path = path_str.clone();
if let Event::Key(_key_event) = event {
// Do NOT call the input engine here again. The top-level
// process_key call above already ran for this key.
// If we are waiting for more leader keys, swallow the key.
info!("Form branch: has_active_seq={}", self.input_engine.has_active_sequence());
if self.input_engine.has_active_sequence() {
info!("Form branch suppressing key {:?}, leader in progress", key_event.code);
return Ok(EventOutcome::Ok(String::new()));
}
// Otherwise, forward to the form editor/canvas.
let outcome = forms::event::handle_form_event( let outcome = forms::event::handle_form_event(
event, event,
app_state, app_state,
path, &path,
&mut self.ideal_cursor_column, &mut self.ideal_cursor_column,
)?; )?;
// Only return if the form page actually consumed the key
if !outcome.get_message_if_ok().is_empty() { if !outcome.get_message_if_ok().is_empty() {
return Ok(outcome); return Ok(outcome);
} }
}
} else if let Page::AddLogic(add_logic_page) = &mut router.current { } else if let Page::AddLogic(add_logic_page) = &mut router.current {
// Allow ":" (enter_command_mode) even when inside AddLogic canvas // Allow ":" (enter_command_mode) even when inside AddLogic canvas
if let Some(action) = if let Some(action) =
@@ -345,7 +430,7 @@ impl EventHandler {
self.command_mode = true; self.command_mode = true;
self.command_input.clear(); self.command_input.clear();
self.command_message.clear(); self.command_message.clear();
self.key_sequence_tracker.reset(); self.input_engine.reset_sequence();
self.set_focus_outside(router, true); self.set_focus_outside(router, true);
return Ok(EventOutcome::Ok(String::new())); return Ok(EventOutcome::Ok(String::new()));
} }
@@ -391,7 +476,7 @@ impl EventHandler {
self.command_mode = true; self.command_mode = true;
self.command_input.clear(); self.command_input.clear();
self.command_message.clear(); self.command_message.clear();
self.key_sequence_tracker.reset(); self.input_engine.reset_sequence();
self.set_focus_outside(router, true); self.set_focus_outside(router, true);
return Ok(EventOutcome::Ok(String::new())); return Ok(EventOutcome::Ok(String::new()));
} }
@@ -447,106 +532,11 @@ impl EventHandler {
} }
} }
} }
if toggle_sidebar(
&mut app_state.ui, // Sidebar/buffer toggles now handled via AppAction in the engine
config,
key_code,
modifiers,
) {
let message = format!(
"Sidebar {}",
if app_state.ui.show_sidebar {
"shown"
} else {
"hidden"
}
);
return Ok(EventOutcome::Ok(message));
}
if toggle_buffer_list(
&mut app_state.ui,
config,
key_code,
modifiers,
) {
let message = format!(
"Buffer {}",
if app_state.ui.show_buffer_list {
"shown"
} else {
"hidden"
}
);
return Ok(EventOutcome::Ok(message));
}
if current_mode == AppMode::General { if current_mode == AppMode::General {
if let Some(action) = config.get_action_for_key_in_mode( // General mode specific key mapping now handled via AppAction
&config.keybindings.global,
key_code,
modifiers,
) {
match action {
"next_buffer" => {
if switch_buffer(buffer_state, true) {
return Ok(EventOutcome::Ok(
"Switched to next buffer".to_string(),
));
}
}
"previous_buffer" => {
if switch_buffer(buffer_state, false) {
return Ok(EventOutcome::Ok(
"Switched to previous buffer".to_string(),
));
}
}
"close_buffer" => {
let current_table_name =
app_state.current_view_table_name.as_deref();
let message = buffer_state
.close_buffer_with_intro_fallback(
current_table_name,
);
return Ok(EventOutcome::Ok(message));
}
_ => {}
}
}
if let Some(action) = config.get_general_action(key_code, modifiers) {
if action == "open_search" {
if let Page::Form(_) = &router.current {
if let Some(table_name) =
app_state.current_view_table_name.clone()
{
app_state.ui.show_search_palette = true;
app_state.search_state =
Some(SearchState::new(table_name));
self.set_focus_outside(router, true);
return Ok(EventOutcome::Ok(
"Search palette opened".to_string(),
));
}
}
}
// Allow ":" / ctrl+; to enter command mode only when outside canvas.
if action == "enter_command_mode" {
if self.is_focus_outside(router)
&& !self.command_mode
&& !app_state.ui.show_search_palette
&& !self.navigation_state.active
{
self.command_mode = true;
self.command_input.clear();
self.command_message.clear();
self.key_sequence_tracker.reset();
// Keep focus outside so canvas won't receive keys
self.set_focus_outside(router, true);
return Ok(EventOutcome::Ok(String::new()));
}
}
}
} }
match current_mode { match current_mode {
@@ -678,146 +668,20 @@ impl EventHandler {
} }
AppMode::Command => { AppMode::Command => {
if config.is_exit_command_mode(key_code, modifiers) { // Command-mode keys already handled by the engine.
self.command_input.clear(); // Collect characters not handled (typed command input).
self.command_message.clear(); match key_code {
self.command_mode = false; KeyCode::Char(c) => {
self.key_sequence_tracker.reset();
if let Page::Form(path) = &router.current {
if let Some(editor) = app_state.editor_for_path(path) {
editor.set_mode(CanvasMode::ReadOnly);
}
}
return Ok(EventOutcome::Ok(
"Exited command mode".to_string(),
));
}
if config.is_command_execute(key_code, modifiers) {
let (mut current_position, total_count) =
if let Page::Form(path) = &router.current {
if let Some(fs) =
app_state.form_state_for_path_ref(path)
{
(fs.current_position, fs.total_count)
} else {
(1, 0)
}
} else {
(1, 0)
};
let outcome = command_mode::handle_command_event(
key_event,
config,
app_state,
router,
&mut self.command_input,
&mut self.command_message,
&mut self.grpc_client,
command_handler,
terminal,
&mut current_position,
total_count,
).await?;
if let Page::Form(path) = &router.current {
if let Some(fs) = app_state.form_state_for_path(path) {
fs.current_position = current_position;
}
}
self.command_mode = false;
self.key_sequence_tracker.reset();
let new_mode = ModeManager::derive_mode(
app_state,
self,
router,
);
app_state.update_mode(new_mode);
return Ok(outcome);
}
if key_code == KeyCode::Backspace {
self.command_input.pop();
self.key_sequence_tracker.reset();
return Ok(EventOutcome::Ok(String::new()));
}
if let KeyCode::Char(c) = key_code {
if c == 'f' {
self.key_sequence_tracker.add_key(key_code);
let sequence =
self.key_sequence_tracker.get_sequence();
if config.matches_key_sequence_generalized(
&sequence,
) == Some("find_file_palette_toggle")
{
if matches!(&router.current, Page::Form(_) | Page::Intro(_)) {
let mut all_table_paths: Vec<String> =
app_state
.profile_tree
.profiles
.iter()
.flat_map(|profile| {
profile.tables.iter().map(
move |table| {
format!(
"{}/{}",
profile.name,
table.name
)
},
)
})
.collect();
all_table_paths.sort();
self.navigation_state
.activate_find_file(all_table_paths);
self.command_mode = false;
self.command_input.clear();
self.command_message.clear();
self.key_sequence_tracker.reset();
return Ok(EventOutcome::Ok(
"Table selection palette activated"
.to_string(),
));
} else {
self.key_sequence_tracker.reset();
self.command_input.push('f');
if sequence.len() > 1
&& sequence[0] == KeyCode::Char('f')
{
self.command_input.push('f');
}
self.command_message = "Find File not available in this view."
.to_string();
return Ok(EventOutcome::Ok(
self.command_message.clone(),
));
}
}
if config.is_key_sequence_prefix(&sequence) {
return Ok(EventOutcome::Ok(String::new()));
}
}
if c != 'f'
&& !self.key_sequence_tracker.current_sequence.is_empty()
{
self.key_sequence_tracker.reset();
}
self.command_input.push(c); self.command_input.push(c);
return Ok(EventOutcome::Ok(String::new())); return Ok(EventOutcome::Ok(String::new()));
} }
_ => {
self.key_sequence_tracker.reset(); self.input_engine.reset_sequence();
return Ok(EventOutcome::Ok(String::new())); return Ok(EventOutcome::Ok(String::new()));
} }
} }
}
}
} else if let Event::Resize(_, _) = event { } else if let Event::Resize(_, _) = event {
return Ok(EventOutcome::Ok("Resized".to_string())); return Ok(EventOutcome::Ok("Resized".to_string()));
} }
@@ -1003,4 +867,227 @@ impl EventHandler {
_ => 0, _ => 0,
} }
} }
async fn execute_command(
&mut self,
key_event: crossterm::event::KeyEvent,
config: &Config,
terminal: &mut TerminalCore,
command_handler: &mut CommandHandler,
app_state: &mut AppState,
router: &mut Router,
) -> Result<EventOutcome> {
let (mut current_position, total_count) = if let Page::Form(path) = &router.current {
if let Some(fs) = app_state.form_state_for_path_ref(path) {
(fs.current_position, fs.total_count)
} else {
(1, 0)
}
} else {
(1, 0)
};
let outcome = command_mode::handle_command_event(
key_event,
config,
app_state,
router,
&mut self.command_input,
&mut self.command_message,
&mut self.grpc_client,
command_handler,
terminal,
&mut current_position,
total_count,
)
.await?;
if let Page::Form(path) = &router.current {
if let Some(fs) = app_state.form_state_for_path(path) {
fs.current_position = current_position;
}
}
self.command_mode = false;
self.input_engine.reset_sequence();
let new_mode = ModeManager::derive_mode(app_state, self, router);
app_state.update_mode(new_mode);
Ok(outcome)
}
#[allow(clippy::too_many_arguments)]
async fn handle_app_action(
&mut self,
action: AppAction,
key_event: crossterm::event::KeyEvent,
config: &Config,
terminal: &mut TerminalCore,
command_handler: &mut CommandHandler,
auth_state: &mut AuthState,
buffer_state: &mut BufferState,
app_state: &mut AppState,
router: &mut Router,
) -> Result<Option<EventOutcome>> {
match action {
AppAction::ToggleSidebar => {
app_state.ui.show_sidebar = !app_state.ui.show_sidebar;
let message = format!(
"Sidebar {}",
if app_state.ui.show_sidebar {
"shown"
} else {
"hidden"
}
);
Ok(Some(EventOutcome::Ok(message)))
}
AppAction::ToggleBufferList => {
app_state.ui.show_buffer_list = !app_state.ui.show_buffer_list;
let message = format!(
"Buffer {}",
if app_state.ui.show_buffer_list {
"shown"
} else {
"hidden"
}
);
Ok(Some(EventOutcome::Ok(message)))
}
AppAction::Buffer(BufferAction::Next) => {
if switch_buffer(buffer_state, true) {
return Ok(Some(EventOutcome::Ok(
"Switched to next buffer".to_string(),
)));
}
Ok(Some(EventOutcome::Ok(String::new())))
}
AppAction::Buffer(BufferAction::Previous) => {
if switch_buffer(buffer_state, false) {
return Ok(Some(EventOutcome::Ok(
"Switched to previous buffer".to_string(),
)));
}
Ok(Some(EventOutcome::Ok(String::new())))
}
AppAction::Buffer(BufferAction::Close) => {
let current_table_name = app_state.current_view_table_name.as_deref();
let message = buffer_state
.close_buffer_with_intro_fallback(current_table_name);
Ok(Some(EventOutcome::Ok(message)))
}
AppAction::OpenSearch => {
if let Page::Form(_) = &router.current {
if let Some(table_name) = app_state.current_view_table_name.clone() {
app_state.ui.show_search_palette = true;
app_state.search_state = Some(SearchState::new(table_name));
self.set_focus_outside(router, true);
return Ok(Some(EventOutcome::Ok(
"Search palette opened".to_string(),
)));
}
}
Ok(Some(EventOutcome::Ok(String::new())))
}
AppAction::FindFilePaletteToggle => {
if matches!(&router.current, Page::Form(_) | Page::Intro(_)) {
let mut all_table_paths: Vec<String> = app_state
.profile_tree
.profiles
.iter()
.flat_map(|profile| {
profile.tables.iter().map(move |table| {
format!("{}/{}", profile.name, table.name)
})
})
.collect();
all_table_paths.sort();
self.navigation_state.activate_find_file(all_table_paths);
self.command_mode = false;
self.command_input.clear();
self.command_message.clear();
self.input_engine.reset_sequence();
return Ok(Some(EventOutcome::Ok(
"Table selection palette activated".to_string(),
)));
}
Ok(Some(EventOutcome::Ok(String::new())))
}
AppAction::EnterCommandMode => {
if !self.is_in_form_edit_mode(router, app_state)
&& !self.command_mode
&& !app_state.ui.show_search_palette
&& !self.navigation_state.active
{
self.command_mode = true;
self.command_input.clear();
self.command_message.clear();
self.input_engine.reset_sequence();
// Keep focus outside so canvas wont consume keystrokes
self.set_focus_outside(router, true);
}
Ok(Some(EventOutcome::Ok(String::new())))
}
AppAction::ExitCommandMode => {
self.command_input.clear();
self.command_message.clear();
self.command_mode = false;
self.input_engine.reset_sequence();
if let Page::Form(path) = &router.current {
if let Some(editor) = app_state.editor_for_path(path) {
editor.set_mode(CanvasMode::ReadOnly);
}
}
Ok(Some(EventOutcome::Ok(
"Exited command mode".to_string(),
)))
}
AppAction::CommandExecute => {
// Execute using the actual configured key that triggered the action
let out = self
.execute_command(
key_event,
config,
terminal,
command_handler,
app_state,
router,
)
.await?;
Ok(Some(out))
}
AppAction::CommandBackspace => {
self.command_input.pop();
self.input_engine.reset_sequence();
Ok(Some(EventOutcome::Ok(String::new())))
}
AppAction::Core(core) => {
let s = match core {
CoreAction::Save => "save",
CoreAction::ForceQuit => "force_quit",
CoreAction::SaveAndQuit => "save_and_quit",
CoreAction::Revert => "revert",
};
let out = self
.handle_core_action(s, auth_state, terminal, app_state, router)
.await?;
Ok(Some(out))
}
AppAction::Navigate(_ma) => {
// Movement is still handled by page/nav code paths that
// follow after PassThrough. We return None here to keep flow.
Ok(None)
}
}
}
fn is_in_form_edit_mode(&self, router: &Router, app_state: &AppState) -> bool {
if let Page::Form(path) = &router.current {
if let Some(editor) = app_state.editor_for_path_ref(path) {
return editor.mode() == CanvasMode::Edit;
}
}
false
}
} }

View File

@@ -1,6 +1,10 @@
// src/pages/forms/state.rs // src/pages/forms/state.rs
use canvas::{DataProvider, AppMode}; use canvas::{DataProvider, AppMode};
#[cfg(feature = "validation")]
use canvas::{CharacterLimits, ValidationConfig, ValidationConfigBuilder};
#[cfg(feature = "validation")]
use canvas::validation::limits::CountMode;
use common::proto::komp_ac::search::search_response::Hit; use common::proto::komp_ac::search::search_response::Hit;
use std::collections::HashMap; use std::collections::HashMap;
@@ -39,6 +43,18 @@ pub struct FormState {
pub autocomplete_loading: bool, pub autocomplete_loading: bool,
pub link_display_map: HashMap<usize, String>, pub link_display_map: HashMap<usize, String>,
pub app_mode: AppMode, pub app_mode: AppMode,
// Validation 1 (character limits) per field. None = no validation for that field.
// Leave room for future rules (patterns, masks, etc.).
pub char_limits: Vec<Option<CharLimitsRule>>,
}
#[cfg(feature = "validation")]
#[derive(Debug, Clone)]
pub struct CharLimitsRule {
pub min: Option<usize>,
pub max: Option<usize>,
pub warn_at: Option<usize>,
pub count_mode: CountMode,
} }
impl FormState { impl FormState {
@@ -56,6 +72,7 @@ impl FormState {
fields: Vec<FieldDefinition>, fields: Vec<FieldDefinition>,
) -> Self { ) -> Self {
let values = vec![String::new(); fields.len()]; let values = vec![String::new(); fields.len()];
let len = values.len();
FormState { FormState {
id: 0, id: 0,
profile_name, profile_name,
@@ -73,6 +90,7 @@ impl FormState {
autocomplete_loading: false, autocomplete_loading: false,
link_display_map: HashMap::new(), link_display_map: HashMap::new(),
app_mode: canvas::AppMode::Edit, app_mode: canvas::AppMode::Edit,
char_limits: vec![None; len],
} }
} }
@@ -256,6 +274,24 @@ impl FormState {
pub fn set_current_cursor_pos(&mut self, pos: usize) { pub fn set_current_cursor_pos(&mut self, pos: usize) {
self.current_cursor_pos = pos; self.current_cursor_pos = pos;
} }
#[cfg(feature = "validation")]
pub fn set_character_limits_rules(
&mut self,
rules: Vec<Option<CharLimitsRule>>,
) {
if rules.len() == self.fields.len() {
self.char_limits = rules;
} else {
tracing::warn!(
"Character limits count {} != field count {} for {}.{}",
rules.len(),
self.fields.len(),
self.profile_name,
self.table_name
);
}
}
} }
// Step 2: Implement DataProvider for FormState // Step 2: Implement DataProvider for FormState
@@ -282,4 +318,26 @@ impl DataProvider for FormState {
fn supports_suggestions(&self, field_index: usize) -> bool { fn supports_suggestions(&self, field_index: usize) -> bool {
self.fields.get(field_index).map(|f| f.is_link).unwrap_or(false) self.fields.get(field_index).map(|f| f.is_link).unwrap_or(false)
} }
// Validation 1: Provide character-limit-based validation to canvas
// Only compiled when the "validation" feature is enabled on canvas.
#[cfg(feature = "validation")]
fn validation_config(&self, index: usize) -> Option<ValidationConfig> {
let rule = self.char_limits.get(index)?.as_ref()?;
let mut limits = match (rule.min, rule.max) {
(Some(min), Some(max)) => CharacterLimits::new_range(min, max),
(None, Some(max)) => CharacterLimits::new(max),
(Some(min), None) => CharacterLimits::new_range(min, usize::MAX),
(None, None) => CharacterLimits::new(usize::MAX),
};
limits = limits.with_count_mode(rule.count_mode);
if let Some(warn) = rule.warn_at {
limits = limits.with_warning_threshold(warn);
}
Some(
ValidationConfigBuilder::new()
.with_character_limits(limits)
.build(),
)
}
} }

View File

@@ -9,6 +9,7 @@ use crate::ui::handlers::context::DialogPurpose;
use common::proto::komp_ac::auth::LoginResponse; use common::proto::komp_ac::auth::LoginResponse;
use crate::pages::login::LoginFormState; use crate::pages::login::LoginFormState;
use crate::state::pages::auth::UserRole; use crate::state::pages::auth::UserRole;
use canvas::DataProvider;
use anyhow::{Context, Result, anyhow}; use anyhow::{Context, Result, anyhow};
use tokio::spawn; use tokio::spawn;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@@ -108,7 +109,19 @@ pub async fn revert(
login_state: &mut LoginFormState, login_state: &mut LoginFormState,
app_state: &mut AppState, app_state: &mut AppState,
) -> String { ) -> String {
// Clear the underlying state
login_state.clear(); login_state.clear();
// Also clear values inside the editors data provider
{
let dp = login_state.editor.data_provider_mut();
dp.set_field_value(0, "".to_string());
dp.set_field_value(1, "".to_string());
dp.set_current_field(0);
dp.set_current_cursor_pos(0);
dp.set_has_unsaved_changes(false);
}
app_state.hide_dialog(); app_state.hide_dialog();
"Login reverted".to_string() "Login reverted".to_string()
} }

View File

@@ -14,12 +14,20 @@ pub struct AuthClient {
impl AuthClient { impl AuthClient {
pub async fn new() -> Result<Self> { pub async fn new() -> Result<Self> {
// Kept for backward compatibility; opens a new connection.
let client = AuthServiceClient::connect("http://[::1]:50051") let client = AuthServiceClient::connect("http://[::1]:50051")
.await .await
.context("Failed to connect to auth service")?; .context("Failed to connect to auth service")?;
Ok(Self { client }) Ok(Self { client })
} }
/// Preferred: reuse an existing Channel (from GrpcClient).
pub async fn with_channel(channel: Channel) -> Result<Self> {
Ok(Self {
client: AuthServiceClient::new(channel),
})
}
/// Login user via gRPC. /// Login user via gRPC.
pub async fn login(&mut self, identifier: String, password: String) -> Result<LoginResponse> { pub async fn login(&mut self, identifier: String, password: String) -> Result<LoginResponse> {
let request = tonic::Request::new(LoginRequest { identifier, password }); let request = tonic::Request::new(LoginRequest { identifier, password });

View File

@@ -2,7 +2,9 @@
use common::proto::komp_ac::common::Empty; use common::proto::komp_ac::common::Empty;
use common::proto::komp_ac::table_structure::table_structure_service_client::TableStructureServiceClient; use common::proto::komp_ac::table_structure::table_structure_service_client::TableStructureServiceClient;
use common::proto::komp_ac::table_structure::{GetTableStructureRequest, TableStructureResponse}; use common::proto::komp_ac::table_structure::{
GetTableStructureRequest, TableStructureResponse,
};
use common::proto::komp_ac::table_definition::{ use common::proto::komp_ac::table_definition::{
table_definition_client::TableDefinitionClient, table_definition_client::TableDefinitionClient,
PostTableDefinitionRequest, ProfileTreeResponse, TableDefinitionResponse, PostTableDefinitionRequest, ProfileTreeResponse, TableDefinitionResponse,
@@ -24,23 +26,41 @@ use common::proto::komp_ac::tables_data::{
}; };
use crate::search::SearchGrpc; use crate::search::SearchGrpc;
use common::proto::komp_ac::search::SearchResponse; use common::proto::komp_ac::search::SearchResponse;
use common::proto::komp_ac::table_validation::{
table_validation_service_client::TableValidationServiceClient,
GetTableValidationRequest,
TableValidationResponse,
CountMode as PbCountMode,
FieldValidation as PbFieldValidation,
CharacterLimits as PbCharacterLimits,
};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::collections::HashMap; use std::collections::HashMap;
use tonic::transport::Channel; use tonic::transport::{Channel, Endpoint};
use prost_types::Value; use prost_types::Value;
use std::time::Duration;
#[derive(Clone)] #[derive(Clone)]
pub struct GrpcClient { pub struct GrpcClient {
channel: Channel,
table_structure_client: TableStructureServiceClient<Channel>, table_structure_client: TableStructureServiceClient<Channel>,
table_definition_client: TableDefinitionClient<Channel>, table_definition_client: TableDefinitionClient<Channel>,
table_script_client: TableScriptClient<Channel>, table_script_client: TableScriptClient<Channel>,
tables_data_client: TablesDataClient<Channel>, tables_data_client: TablesDataClient<Channel>,
search_client: SearchGrpc, search_client: SearchGrpc,
table_validation_client: TableValidationServiceClient<Channel>,
} }
impl GrpcClient { impl GrpcClient {
pub async fn new() -> Result<Self> { pub async fn new() -> Result<Self> {
let channel = Channel::from_static("http://[::1]:50051") let endpoint = Endpoint::from_static("http://[::1]:50051")
.connect_timeout(Duration::from_secs(5))
.tcp_keepalive(Some(Duration::from_secs(30)))
.keep_alive_while_idle(true)
.http2_keep_alive_interval(Duration::from_secs(15))
.keep_alive_timeout(Duration::from_secs(5));
let channel = endpoint
.connect() .connect()
.await .await
.context("Failed to create gRPC channel")?; .context("Failed to create gRPC channel")?;
@@ -52,16 +72,43 @@ impl GrpcClient {
let table_script_client = TableScriptClient::new(channel.clone()); let table_script_client = TableScriptClient::new(channel.clone());
let tables_data_client = TablesDataClient::new(channel.clone()); let tables_data_client = TablesDataClient::new(channel.clone());
let search_client = SearchGrpc::new(channel.clone()); let search_client = SearchGrpc::new(channel.clone());
let table_validation_client =
TableValidationServiceClient::new(channel.clone());
Ok(Self { Ok(Self {
channel,
table_structure_client, table_structure_client,
table_definition_client, table_definition_client,
table_script_client, table_script_client,
tables_data_client, tables_data_client,
search_client, search_client,
table_validation_client,
}) })
} }
// Expose the shared channel so other typed clients can reuse it.
pub fn channel(&self) -> Channel {
self.channel.clone()
}
// Fetch validation rules for a table. Absence of a field in response = no validation.
pub async fn get_table_validation(
&mut self,
profile_name: String,
table_name: String,
) -> Result<TableValidationResponse> {
let req = GetTableValidationRequest {
profile_name,
table_name,
};
let resp = self
.table_validation_client
.get_table_validation(tonic::Request::new(req))
.await
.context("gRPC GetTableValidation call failed")?;
Ok(resp.into_inner())
}
pub async fn get_table_structure( pub async fn get_table_structure(
&mut self, &mut self,
profile_name: String, profile_name: String,

View File

@@ -6,6 +6,8 @@ use crate::pages::admin_panel::add_logic::state::AddLogicState;
use crate::pages::forms::logic::SaveOutcome; use crate::pages::forms::logic::SaveOutcome;
use crate::utils::columns::filter_user_columns; use crate::utils::columns::filter_user_columns;
use crate::pages::forms::{FieldDefinition, FormState}; use crate::pages::forms::{FieldDefinition, FormState};
use common::proto::komp_ac::table_validation::CountMode as PbCountMode;
use canvas::validation::limits::CountMode;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use std::sync::Arc; use std::sync::Arc;
@@ -314,4 +316,60 @@ impl UiService {
} }
Ok(()) Ok(())
} }
/// Fetch and apply "Validation 1" (character limits) rules for this form.
pub async fn apply_validation1_for_form(
grpc_client: &mut GrpcClient,
app_state: &mut AppState,
path: &str,
) -> Result<()> {
let (profile, table) = path
.split_once('/')
.context("Invalid form path for validation")?;
let resp = grpc_client
.get_table_validation(profile.to_string(), table.to_string())
.await
.context("Failed to fetch table validation")?;
if let Some(fs) = app_state.form_state_for_path(path) {
let mut rules: Vec<Option<crate::pages::forms::state::CharLimitsRule>> =
vec![None; fs.fields.len()];
for f in resp.fields {
if let Some(idx) = fs.fields.iter().position(|fd| fd.data_key == f.data_key) {
if let Some(limits) = f.limits {
let has_any =
limits.min != 0 || limits.max != 0 || limits.warn_at.is_some();
if has_any {
let cm = match PbCountMode::from_i32(limits.count_mode) {
Some(PbCountMode::Unspecified) | None => CountMode::Characters, // protobuf default → fallback
Some(PbCountMode::Chars) => CountMode::Characters,
Some(PbCountMode::Bytes) => CountMode::Bytes,
Some(PbCountMode::DisplayWidth) => CountMode::DisplayWidth,
};
let min = if limits.min == 0 { None } else { Some(limits.min as usize) };
let max = if limits.max == 0 { None } else { Some(limits.max as usize) };
let warn_at = limits.warn_at.map(|w| w as usize);
rules[idx] = Some(crate::pages::forms::state::CharLimitsRule {
min,
max,
warn_at,
count_mode: cm,
});
}
}
}
}
fs.set_character_limits_rules(rules);
}
if let Some(editor) = app_state.editor_for_path(path) {
editor.set_validation_enabled(true);
}
Ok(())
}
} }

View File

@@ -59,7 +59,6 @@ pub struct AppState {
pub ui: UiState, pub ui: UiState,
pub form_editor: HashMap<String, FormEditor<FormState>>, // key = "profile/table" pub form_editor: HashMap<String, FormEditor<FormState>>, // key = "profile/table"
#[cfg(feature = "ui-debug")] #[cfg(feature = "ui-debug")]
pub debug_state: Option<DebugState>, pub debug_state: Option<DebugState>,
} }

View File

@@ -26,6 +26,7 @@ use crate::buffer::state::AppView;
use crate::state::app::state::AppState; use crate::state::app::state::AppState;
use crate::tui::terminal::{EventReader, TerminalCore}; use crate::tui::terminal::{EventReader, TerminalCore};
use crate::ui::handlers::render::render_ui; use crate::ui::handlers::render::render_ui;
use crate::input::leader::leader_has_any_start;
use crate::pages::login; use crate::pages::login;
use crate::pages::register; use crate::pages::register;
use crate::pages::login::LoginResult; use crate::pages::login::LoginResult;
@@ -122,6 +123,10 @@ pub async fn run_ui() -> Result<()> {
app_state.ensure_form_editor(&path, &config, || { app_state.ensure_form_editor(&path, &config, || {
FormState::new(initial_profile.clone(), initial_table.clone(), initial_field_defs) FormState::new(initial_profile.clone(), initial_table.clone(), initial_field_defs)
}); });
#[cfg(feature = "validation")]
UiService::apply_validation1_for_form(&mut grpc_client, &mut app_state, &path)
.await
.ok();
buffer_state.update_history(AppView::Form(path.clone())); buffer_state.update_history(AppView::Form(path.clone()));
router.navigate(Page::Form(path.clone())); router.navigate(Page::Form(path.clone()));
@@ -237,6 +242,27 @@ pub async fn run_ui() -> Result<()> {
}; };
if inside_canvas { if inside_canvas {
// Do NOT forward to canvas while a leader is active or about to start.
// This prevents the canvas from stealing the second/third key (b/d/r).
let leader_in_progress = event_handler.input_engine.has_active_sequence();
let is_space = matches!(key_event.code, crossterm_event::KeyCode::Char(' '));
let can_start_leader = leader_has_any_start(&config);
let form_in_edit_mode = match &router.current {
Page::Form(path) => app_state
.editor_for_path_ref(path)
.map(|e| e.mode() == canvas::AppMode::Edit)
.unwrap_or(false),
_ => false,
};
let defer_to_engine_for_leader = leader_in_progress
|| (is_space && can_start_leader && !form_in_edit_mode);
if defer_to_engine_for_leader {
info!(
"Skipping canvas pre-handle: leader sequence active or starting"
);
} else {
if let Page::Form(path) = &router.current { if let Page::Form(path) = &router.current {
if let Some(editor) = app_state.editor_for_path(path) { if let Some(editor) = app_state.editor_for_path(path) {
match editor.handle_key_event(*key_event) { match editor.handle_key_event(*key_event) {
@@ -262,6 +288,7 @@ pub async fn run_ui() -> Result<()> {
} }
} }
} }
}
// Call handle_event directly // Call handle_event directly
let event_outcome_result = event_handler.handle_event( let event_outcome_result = event_handler.handle_event(
@@ -493,6 +520,21 @@ pub async fn run_ui() -> Result<()> {
prev_view_profile_name = current_view_profile; prev_view_profile_name = current_view_profile;
prev_view_table_name = current_view_table; prev_view_table_name = current_view_table;
table_just_switched = true; table_just_switched = true;
// Apply character-limit validation for the new form
#[cfg(feature = "validation")]
if let (Some(prof), Some(tbl)) = (
app_state.current_view_profile_name.as_ref(),
app_state.current_view_table_name.as_ref(),
) {
let p = format!("{}/{}", prof, tbl);
UiService::apply_validation1_for_form(
&mut grpc_client,
&mut app_state,
&p,
)
.await
.ok();
}
} }
Err(e) => { Err(e) => {
app_state.update_dialog_content( app_state.update_dialog_content(

View File

@@ -1,8 +1,11 @@
// client/src/utils/debug_logger.rs // client/src/utils/debug_logger.rs
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::collections::VecDeque; // <-- FIX: Import VecDeque use std::collections::VecDeque; // <-- FIX: Import VecDeque
use std::io; use std::io::{self, Write};
use std::sync::{Arc, Mutex}; // <-- FIX: Import Mutex use std::sync::{Arc, Mutex}; // <-- FIX: Import Mutex
use std::fs::OpenOptions;
use std::thread;
use std::time::Duration;
lazy_static! { lazy_static! {
static ref UI_DEBUG_BUFFER: Arc<Mutex<VecDeque<(String, bool)>>> = static ref UI_DEBUG_BUFFER: Arc<Mutex<VecDeque<(String, bool)>>> =
@@ -27,11 +30,21 @@ impl UiDebugWriter {
impl io::Write for UiDebugWriter { impl io::Write for UiDebugWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> { fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let mut buffer = UI_DEBUG_BUFFER.lock().unwrap(); let mut buffer = UI_DEBUG_BUFFER.lock().unwrap();
let message = String::from_utf8_lossy(buf); let message = String::from_utf8_lossy(buf).trim().to_string();
let trimmed_message = message.trim().to_string(); let is_error = message.starts_with("ERROR");
let is_error = trimmed_message.starts_with("ERROR");
// Add the new message to the back of the queue // Keep in memory for UI
buffer.push_back((trimmed_message, is_error)); buffer.push_back((message.clone(), is_error));
// ALSO log directly to file (non-blocking best effort)
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open("ui_debug.log")
{
let _ = writeln!(file, "{message}");
}
Ok(buf.len()) Ok(buf.len())
} }
@@ -44,3 +57,22 @@ impl io::Write for UiDebugWriter {
pub fn pop_next_debug_message() -> Option<(String, bool)> { pub fn pop_next_debug_message() -> Option<(String, bool)> {
UI_DEBUG_BUFFER.lock().unwrap().pop_front() UI_DEBUG_BUFFER.lock().unwrap().pop_front()
} }
/// spawn a background thread that keeps draining UI_DEBUG_BUFFER
/// and writes messages into ui_debug.log continuously
pub fn spawn_file_logger() {
thread::spawn(|| loop {
// pop one message if present
if let Some((msg, _)) = pop_next_debug_message() {
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open("ui_debug.log")
{
let _ = writeln!(file, "{msg}");
}
}
// small sleep to avoid burning CPU
thread::sleep(Duration::from_millis(50));
});
}

View File

@@ -0,0 +1,39 @@
use client::config::binds::config::Config;
use client::input::engine::{InputEngine, InputContext, InputOutcome};
use client::modes::handlers::mode_manager::AppMode;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
fn ctx() -> InputContext {
InputContext {
app_mode: AppMode::General,
overlay_active: false,
allow_navigation_capture: true,
}
}
fn key(c: char) -> KeyEvent {
KeyEvent::new(KeyCode::Char(c), KeyModifiers::empty())
}
#[test]
fn engine_collects_space_b_r() {
let toml_str = r#"
[keybindings]
revert = ["space+b+r"]
"#;
let config: Config = toml::from_str(toml_str).unwrap();
let mut eng = InputEngine::new(400, 5_000);
// space -> Pending (leader started)
let out1 = eng.process_key(key(' '), &ctx(), &config);
assert!(matches!(out1, InputOutcome::Pending));
// b -> Pending (prefix)
let out2 = eng.process_key(key('b'), &ctx(), &config);
assert!(matches!(out2, InputOutcome::Pending));
// r -> Action(revert)
let out3 = eng.process_key(key('r'), &ctx(), &config);
assert!(matches!(out3, InputOutcome::Action(_)));
}

View File

@@ -0,0 +1,25 @@
use client::config::binds::config::Config;
use client::input::leader::leader_match_action;
use client::config::binds::key_sequences::parse_binding;
use crossterm::event::KeyCode;
#[test]
fn test_space_b_d_binding() {
// Minimal fake config TOML
let toml_str = r#"
[keybindings]
close_buffer = ["space+b+d"]
"#;
let config: Config = toml::from_str(toml_str).unwrap();
let seq = vec![KeyCode::Char(' '), KeyCode::Char('b'), KeyCode::Char('d')];
let action = leader_match_action(&config, &seq);
assert_eq!(action, Some("close_buffer"));
}
#[test]
fn parses_space_b_r() {
let seq = parse_binding("space+b+r");
let codes: Vec<KeyCode> = seq.iter().map(|p| p.code).collect();
assert_eq!(codes, vec![KeyCode::Char(' '), KeyCode::Char('b'), KeyCode::Char('r')]);
}

View File

@@ -0,0 +1,4 @@
// tests/input/mod.rs
pub mod engine_leader_e2e;
pub mod leader_sequences;

View File

@@ -1,3 +1,4 @@
// tests/mod.rs // tests/mod.rs
pub mod form; // pub mod form;
pub mod input;

View File

View File

@@ -13,6 +13,8 @@ serde = { version = "1.0.219", features = ["derive"] }
# Search # Search
tantivy = { workspace = true } tantivy = { workspace = true }
serde_json.workspace = true
[build-dependencies] [build-dependencies]
tonic-build = "0.13.0" tonic-build = { version = "0.13.0", features = ["prost-build"] }
prost-build = "0.14.1"

View File

@@ -1,21 +1,75 @@
// common/build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::configure() tonic_build::configure()
.build_server(true) .build_server(true)
.out_dir("src/proto")
.file_descriptor_set_path("src/proto/descriptor.bin") .file_descriptor_set_path("src/proto/descriptor.bin")
.compile_protos( // Changed from .compile() .out_dir("src/proto")
// Derive serde for the messages
.type_attribute(
".komp_ac.table_validation.FieldValidation",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.CharacterLimits",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.DisplayMask",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.TableValidationResponse",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.UpdateFieldValidationRequest",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.UpdateFieldValidationResponse",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
// Enum -> readable strings in JSON ("BYTES", "DISPLAY_WIDTH")
.type_attribute(
".komp_ac.table_validation.CountMode",
"#[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = \"SCREAMING_SNAKE_CASE\")]",
)
.type_attribute(
".komp_ac.table_definition.ColumnDefinition",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_definition.TableLink",
"#[derive(serde::Serialize, serde::Deserialize)]"
)
.type_attribute(
".komp_ac.table_definition.PostTableDefinitionRequest",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_definition.TableDefinitionResponse",
"#[derive(serde::Serialize, serde::Deserialize)]"
)
.type_attribute(
".komp_ac.table_script.PostTableScriptRequest",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_script.TableScriptResponse",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.compile_protos(
&[ &[
"proto/common.proto", "proto/common.proto",
"proto/adresar.proto", "proto/adresar.proto",
"proto/auth.proto", "proto/auth.proto",
"proto/uctovnictvo.proto",
"proto/table_structure.proto",
"proto/table_definition.proto",
"proto/tables_data.proto",
"proto/table_script.proto",
"proto/search.proto", "proto/search.proto",
"proto/search2.proto", "proto/search2.proto",
"proto/table_definition.proto",
"proto/table_script.proto",
"proto/table_structure.proto",
"proto/table_validation.proto",
"proto/tables_data.proto",
"proto/uctovnictvo.proto",
], ],
&["proto"], &["proto"],
)?; )?;

View File

@@ -0,0 +1,79 @@
// common/proto/table_validation.proto
syntax = "proto3";
package komp_ac.table_validation;
// Request validation rules for a table
message GetTableValidationRequest {
string profileName = 1;
string tableName = 2;
}
// Response with field-level validations; if a field is omitted,
// no validation is applied (default unspecified).
message TableValidationResponse {
repeated FieldValidation fields = 1;
}
// Field-level validation (extensible for future kinds)
message FieldValidation {
// MUST match your frontend FormState.dataKey for the column
string dataKey = 1;
// Current: only CharacterLimits. More rules can be added later.
CharacterLimits limits = 10;
// Future expansion:
// PatternRules pattern = 11;
DisplayMask mask = 3;
// ExternalValidation external = 13;
// CustomFormatter formatter = 14;
bool required = 4;
}
// Character length counting mode
enum CountMode {
COUNT_MODE_UNSPECIFIED = 0; // default: same as CHARS
CHARS = 1;
BYTES = 2;
DISPLAY_WIDTH = 3;
}
// Character limit validation (Validation 1)
message CharacterLimits {
// When zero, the field is considered "not set". If both min/max are zero,
// the server should avoid sending this FieldValidation (no validation).
uint32 min = 1;
uint32 max = 2;
// Optional warning threshold; when unset, no warning threshold is applied.
optional uint32 warnAt = 3;
CountMode countMode = 4; // defaults to CHARS if unspecified
}
// Mask for pretty display
message DisplayMask {
string pattern = 1; // e.g., "(###) ###-####" or "####-##-##"
string input_char = 2; // e.g., "#"
optional string template_char = 3; // e.g., "_"
}
// Service to fetch validations for a table
service TableValidationService {
rpc GetTableValidation(GetTableValidationRequest)
returns (TableValidationResponse);
rpc UpdateFieldValidation(UpdateFieldValidationRequest)
returns (UpdateFieldValidationResponse);
}
message UpdateFieldValidationRequest {
string profileName = 1;
string tableName = 2;
string dataKey = 3;
FieldValidation validation = 4;
}
message UpdateFieldValidationResponse {
bool success = 1;
string message = 2;
}

View File

@@ -34,6 +34,9 @@ pub mod proto {
pub mod search2 { pub mod search2 {
include!("proto/komp_ac.search2.rs"); include!("proto/komp_ac.search2.rs");
} }
pub mod table_validation {
include!("proto/komp_ac.table_validation.rs");
}
pub const FILE_DESCRIPTOR_SET: &[u8] = pub const FILE_DESCRIPTOR_SET: &[u8] =
include_bytes!("proto/descriptor.bin"); include_bytes!("proto/descriptor.bin");
} }

Binary file not shown.

View File

@@ -1,4 +1,5 @@
// This file is @generated by prost-build. // This file is @generated by prost-build.
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct TableLink { pub struct TableLink {
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
@@ -6,6 +7,7 @@ pub struct TableLink {
#[prost(bool, tag = "2")] #[prost(bool, tag = "2")]
pub required: bool, pub required: bool,
} }
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct PostTableDefinitionRequest { pub struct PostTableDefinitionRequest {
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
@@ -19,6 +21,7 @@ pub struct PostTableDefinitionRequest {
#[prost(string, tag = "5")] #[prost(string, tag = "5")]
pub profile_name: ::prost::alloc::string::String, pub profile_name: ::prost::alloc::string::String,
} }
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ColumnDefinition { pub struct ColumnDefinition {
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
@@ -26,6 +29,7 @@ pub struct ColumnDefinition {
#[prost(string, tag = "2")] #[prost(string, tag = "2")]
pub field_type: ::prost::alloc::string::String, pub field_type: ::prost::alloc::string::String,
} }
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct TableDefinitionResponse { pub struct TableDefinitionResponse {
#[prost(bool, tag = "1")] #[prost(bool, tag = "1")]

View File

@@ -1,4 +1,5 @@
// This file is @generated by prost-build. // This file is @generated by prost-build.
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct PostTableScriptRequest { pub struct PostTableScriptRequest {
#[prost(int64, tag = "1")] #[prost(int64, tag = "1")]
@@ -10,6 +11,7 @@ pub struct PostTableScriptRequest {
#[prost(string, tag = "4")] #[prost(string, tag = "4")]
pub description: ::prost::alloc::string::String, pub description: ::prost::alloc::string::String,
} }
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct TableScriptResponse { pub struct TableScriptResponse {
#[prost(int64, tag = "1")] #[prost(int64, tag = "1")]

View File

@@ -1,78 +1,128 @@
// This file is @generated by prost-build. // This file is @generated by prost-build.
/// Request validation rules for a table
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct TableLink { pub struct GetTableValidationRequest {
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
pub linked_table_name: ::prost::alloc::string::String, pub profile_name: ::prost::alloc::string::String,
#[prost(bool, tag = "2")] #[prost(string, tag = "2")]
pub table_name: ::prost::alloc::string::String,
}
/// Response with field-level validations; if a field is omitted,
/// no validation is applied (default unspecified).
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TableValidationResponse {
#[prost(message, repeated, tag = "1")]
pub fields: ::prost::alloc::vec::Vec<FieldValidation>,
}
/// Field-level validation (extensible for future kinds)
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct FieldValidation {
/// MUST match your frontend FormState.dataKey for the column
#[prost(string, tag = "1")]
pub data_key: ::prost::alloc::string::String,
/// Current: only CharacterLimits. More rules can be added later.
#[prost(message, optional, tag = "10")]
pub limits: ::core::option::Option<CharacterLimits>,
/// Future expansion:
/// PatternRules pattern = 11;
#[prost(message, optional, tag = "3")]
pub mask: ::core::option::Option<DisplayMask>,
/// ExternalValidation external = 13;
/// CustomFormatter formatter = 14;
#[prost(bool, tag = "4")]
pub required: bool, pub required: bool,
} }
/// Character limit validation (Validation 1)
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct CharacterLimits {
/// When zero, the field is considered "not set". If both min/max are zero,
/// the server should avoid sending this FieldValidation (no validation).
#[prost(uint32, tag = "1")]
pub min: u32,
#[prost(uint32, tag = "2")]
pub max: u32,
/// Optional warning threshold; when unset, no warning threshold is applied.
#[prost(uint32, optional, tag = "3")]
pub warn_at: ::core::option::Option<u32>,
/// defaults to CHARS if unspecified
#[prost(enumeration = "CountMode", tag = "4")]
pub count_mode: i32,
}
/// Mask for pretty display
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct PostTableDefinitionRequest { pub struct DisplayMask {
/// e.g., "(###) ###-####" or "####-##-##"
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
pub table_name: ::prost::alloc::string::String, pub pattern: ::prost::alloc::string::String,
#[prost(message, repeated, tag = "2")] /// e.g., "#"
pub links: ::prost::alloc::vec::Vec<TableLink>,
#[prost(message, repeated, tag = "3")]
pub columns: ::prost::alloc::vec::Vec<ColumnDefinition>,
#[prost(string, repeated, tag = "4")]
pub indexes: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
#[prost(string, tag = "5")]
pub profile_name: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ColumnDefinition {
#[prost(string, tag = "1")]
pub name: ::prost::alloc::string::String,
#[prost(string, tag = "2")] #[prost(string, tag = "2")]
pub field_type: ::prost::alloc::string::String, pub input_char: ::prost::alloc::string::String,
/// e.g., "_"
#[prost(string, optional, tag = "3")]
pub template_char: ::core::option::Option<::prost::alloc::string::String>,
} }
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct TableDefinitionResponse { pub struct UpdateFieldValidationRequest {
#[prost(bool, tag = "1")]
pub success: bool,
#[prost(string, tag = "2")]
pub sql: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProfileTreeResponse {
#[prost(message, repeated, tag = "1")]
pub profiles: ::prost::alloc::vec::Vec<profile_tree_response::Profile>,
}
/// Nested message and enum types in `ProfileTreeResponse`.
pub mod profile_tree_response {
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Table {
#[prost(int64, tag = "1")]
pub id: i64,
#[prost(string, tag = "2")]
pub name: ::prost::alloc::string::String,
#[prost(string, repeated, tag = "3")]
pub depends_on: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Profile {
#[prost(string, tag = "1")]
pub name: ::prost::alloc::string::String,
#[prost(message, repeated, tag = "2")]
pub tables: ::prost::alloc::vec::Vec<Table>,
}
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct DeleteTableRequest {
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String, pub profile_name: ::prost::alloc::string::String,
#[prost(string, tag = "2")] #[prost(string, tag = "2")]
pub table_name: ::prost::alloc::string::String, pub table_name: ::prost::alloc::string::String,
#[prost(string, tag = "3")]
pub data_key: ::prost::alloc::string::String,
#[prost(message, optional, tag = "4")]
pub validation: ::core::option::Option<FieldValidation>,
} }
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct DeleteTableResponse { pub struct UpdateFieldValidationResponse {
#[prost(bool, tag = "1")] #[prost(bool, tag = "1")]
pub success: bool, pub success: bool,
#[prost(string, tag = "2")] #[prost(string, tag = "2")]
pub message: ::prost::alloc::string::String, pub message: ::prost::alloc::string::String,
} }
/// Character length counting mode
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum CountMode {
/// default: same as CHARS
Unspecified = 0,
Chars = 1,
Bytes = 2,
DisplayWidth = 3,
}
impl CountMode {
/// String value of the enum field names used in the ProtoBuf definition.
///
/// The values are not transformed in any way and thus are considered stable
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
pub fn as_str_name(&self) -> &'static str {
match self {
Self::Unspecified => "COUNT_MODE_UNSPECIFIED",
Self::Chars => "CHARS",
Self::Bytes => "BYTES",
Self::DisplayWidth => "DISPLAY_WIDTH",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"COUNT_MODE_UNSPECIFIED" => Some(Self::Unspecified),
"CHARS" => Some(Self::Chars),
"BYTES" => Some(Self::Bytes),
"DISPLAY_WIDTH" => Some(Self::DisplayWidth),
_ => None,
}
}
}
/// Generated client implementations. /// Generated client implementations.
pub mod table_definition_client { pub mod table_validation_service_client {
#![allow( #![allow(
unused_variables, unused_variables,
dead_code, dead_code,
@@ -82,11 +132,12 @@ pub mod table_definition_client {
)] )]
use tonic::codegen::*; use tonic::codegen::*;
use tonic::codegen::http::Uri; use tonic::codegen::http::Uri;
/// Service to fetch validations for a table
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TableDefinitionClient<T> { pub struct TableValidationServiceClient<T> {
inner: tonic::client::Grpc<T>, inner: tonic::client::Grpc<T>,
} }
impl TableDefinitionClient<tonic::transport::Channel> { impl TableValidationServiceClient<tonic::transport::Channel> {
/// Attempt to create a new client by connecting to a given endpoint. /// Attempt to create a new client by connecting to a given endpoint.
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error> pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
where where
@@ -97,7 +148,7 @@ pub mod table_definition_client {
Ok(Self::new(conn)) Ok(Self::new(conn))
} }
} }
impl<T> TableDefinitionClient<T> impl<T> TableValidationServiceClient<T>
where where
T: tonic::client::GrpcService<tonic::body::Body>, T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>, T::Error: Into<StdError>,
@@ -115,7 +166,7 @@ pub mod table_definition_client {
pub fn with_interceptor<F>( pub fn with_interceptor<F>(
inner: T, inner: T,
interceptor: F, interceptor: F,
) -> TableDefinitionClient<InterceptedService<T, F>> ) -> TableValidationServiceClient<InterceptedService<T, F>>
where where
F: tonic::service::Interceptor, F: tonic::service::Interceptor,
T::ResponseBody: Default, T::ResponseBody: Default,
@@ -129,7 +180,9 @@ pub mod table_definition_client {
http::Request<tonic::body::Body>, http::Request<tonic::body::Body>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync, >>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{ {
TableDefinitionClient::new(InterceptedService::new(inner, interceptor)) TableValidationServiceClient::new(
InterceptedService::new(inner, interceptor),
)
} }
/// Compress requests with the given encoding. /// Compress requests with the given encoding.
/// ///
@@ -162,11 +215,11 @@ pub mod table_definition_client {
self.inner = self.inner.max_encoding_message_size(limit); self.inner = self.inner.max_encoding_message_size(limit);
self self
} }
pub async fn post_table_definition( pub async fn get_table_validation(
&mut self, &mut self,
request: impl tonic::IntoRequest<super::PostTableDefinitionRequest>, request: impl tonic::IntoRequest<super::GetTableValidationRequest>,
) -> std::result::Result< ) -> std::result::Result<
tonic::Response<super::TableDefinitionResponse>, tonic::Response<super::TableValidationResponse>,
tonic::Status, tonic::Status,
> { > {
self.inner self.inner
@@ -179,23 +232,23 @@ pub mod table_definition_client {
})?; })?;
let codec = tonic::codec::ProstCodec::default(); let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static( let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_definition.TableDefinition/PostTableDefinition", "/komp_ac.table_validation.TableValidationService/GetTableValidation",
); );
let mut req = request.into_request(); let mut req = request.into_request();
req.extensions_mut() req.extensions_mut()
.insert( .insert(
GrpcMethod::new( GrpcMethod::new(
"komp_ac.table_definition.TableDefinition", "komp_ac.table_validation.TableValidationService",
"PostTableDefinition", "GetTableValidation",
), ),
); );
self.inner.unary(req, path, codec).await self.inner.unary(req, path, codec).await
} }
pub async fn get_profile_tree( pub async fn update_field_validation(
&mut self, &mut self,
request: impl tonic::IntoRequest<super::super::common::Empty>, request: impl tonic::IntoRequest<super::UpdateFieldValidationRequest>,
) -> std::result::Result< ) -> std::result::Result<
tonic::Response<super::ProfileTreeResponse>, tonic::Response<super::UpdateFieldValidationResponse>,
tonic::Status, tonic::Status,
> { > {
self.inner self.inner
@@ -208,43 +261,14 @@ pub mod table_definition_client {
})?; })?;
let codec = tonic::codec::ProstCodec::default(); let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static( let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_definition.TableDefinition/GetProfileTree", "/komp_ac.table_validation.TableValidationService/UpdateFieldValidation",
); );
let mut req = request.into_request(); let mut req = request.into_request();
req.extensions_mut() req.extensions_mut()
.insert( .insert(
GrpcMethod::new( GrpcMethod::new(
"komp_ac.table_definition.TableDefinition", "komp_ac.table_validation.TableValidationService",
"GetProfileTree", "UpdateFieldValidation",
),
);
self.inner.unary(req, path, codec).await
}
pub async fn delete_table(
&mut self,
request: impl tonic::IntoRequest<super::DeleteTableRequest>,
) -> std::result::Result<
tonic::Response<super::DeleteTableResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_definition.TableDefinition/DeleteTable",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"komp_ac.table_definition.TableDefinition",
"DeleteTable",
), ),
); );
self.inner.unary(req, path, codec).await self.inner.unary(req, path, codec).await
@@ -252,7 +276,7 @@ pub mod table_definition_client {
} }
} }
/// Generated server implementations. /// Generated server implementations.
pub mod table_definition_server { pub mod table_validation_service_server {
#![allow( #![allow(
unused_variables, unused_variables,
dead_code, dead_code,
@@ -261,40 +285,34 @@ pub mod table_definition_server {
clippy::let_unit_value, clippy::let_unit_value,
)] )]
use tonic::codegen::*; use tonic::codegen::*;
/// Generated trait containing gRPC methods that should be implemented for use with TableDefinitionServer. /// Generated trait containing gRPC methods that should be implemented for use with TableValidationServiceServer.
#[async_trait] #[async_trait]
pub trait TableDefinition: std::marker::Send + std::marker::Sync + 'static { pub trait TableValidationService: std::marker::Send + std::marker::Sync + 'static {
async fn post_table_definition( async fn get_table_validation(
&self, &self,
request: tonic::Request<super::PostTableDefinitionRequest>, request: tonic::Request<super::GetTableValidationRequest>,
) -> std::result::Result< ) -> std::result::Result<
tonic::Response<super::TableDefinitionResponse>, tonic::Response<super::TableValidationResponse>,
tonic::Status, tonic::Status,
>; >;
async fn get_profile_tree( async fn update_field_validation(
&self, &self,
request: tonic::Request<super::super::common::Empty>, request: tonic::Request<super::UpdateFieldValidationRequest>,
) -> std::result::Result< ) -> std::result::Result<
tonic::Response<super::ProfileTreeResponse>, tonic::Response<super::UpdateFieldValidationResponse>,
tonic::Status,
>;
async fn delete_table(
&self,
request: tonic::Request<super::DeleteTableRequest>,
) -> std::result::Result<
tonic::Response<super::DeleteTableResponse>,
tonic::Status, tonic::Status,
>; >;
} }
/// Service to fetch validations for a table
#[derive(Debug)] #[derive(Debug)]
pub struct TableDefinitionServer<T> { pub struct TableValidationServiceServer<T> {
inner: Arc<T>, inner: Arc<T>,
accept_compression_encodings: EnabledCompressionEncodings, accept_compression_encodings: EnabledCompressionEncodings,
send_compression_encodings: EnabledCompressionEncodings, send_compression_encodings: EnabledCompressionEncodings,
max_decoding_message_size: Option<usize>, max_decoding_message_size: Option<usize>,
max_encoding_message_size: Option<usize>, max_encoding_message_size: Option<usize>,
} }
impl<T> TableDefinitionServer<T> { impl<T> TableValidationServiceServer<T> {
pub fn new(inner: T) -> Self { pub fn new(inner: T) -> Self {
Self::from_arc(Arc::new(inner)) Self::from_arc(Arc::new(inner))
} }
@@ -345,9 +363,10 @@ pub mod table_definition_server {
self self
} }
} }
impl<T, B> tonic::codegen::Service<http::Request<B>> for TableDefinitionServer<T> impl<T, B> tonic::codegen::Service<http::Request<B>>
for TableValidationServiceServer<T>
where where
T: TableDefinition, T: TableValidationService,
B: Body + std::marker::Send + 'static, B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static, B::Error: Into<StdError> + std::marker::Send + 'static,
{ {
@@ -362,25 +381,25 @@ pub mod table_definition_server {
} }
fn call(&mut self, req: http::Request<B>) -> Self::Future { fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() { match req.uri().path() {
"/komp_ac.table_definition.TableDefinition/PostTableDefinition" => { "/komp_ac.table_validation.TableValidationService/GetTableValidation" => {
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
struct PostTableDefinitionSvc<T: TableDefinition>(pub Arc<T>); struct GetTableValidationSvc<T: TableValidationService>(pub Arc<T>);
impl< impl<
T: TableDefinition, T: TableValidationService,
> tonic::server::UnaryService<super::PostTableDefinitionRequest> > tonic::server::UnaryService<super::GetTableValidationRequest>
for PostTableDefinitionSvc<T> { for GetTableValidationSvc<T> {
type Response = super::TableDefinitionResponse; type Response = super::TableValidationResponse;
type Future = BoxFuture< type Future = BoxFuture<
tonic::Response<Self::Response>, tonic::Response<Self::Response>,
tonic::Status, tonic::Status,
>; >;
fn call( fn call(
&mut self, &mut self,
request: tonic::Request<super::PostTableDefinitionRequest>, request: tonic::Request<super::GetTableValidationRequest>,
) -> Self::Future { ) -> Self::Future {
let inner = Arc::clone(&self.0); let inner = Arc::clone(&self.0);
let fut = async move { let fut = async move {
<T as TableDefinition>::post_table_definition( <T as TableValidationService>::get_table_validation(
&inner, &inner,
request, request,
) )
@@ -395,7 +414,7 @@ pub mod table_definition_server {
let max_encoding_message_size = self.max_encoding_message_size; let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone(); let inner = self.inner.clone();
let fut = async move { let fut = async move {
let method = PostTableDefinitionSvc(inner); let method = GetTableValidationSvc(inner);
let codec = tonic::codec::ProstCodec::default(); let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec) let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config( .apply_compression_config(
@@ -411,25 +430,30 @@ pub mod table_definition_server {
}; };
Box::pin(fut) Box::pin(fut)
} }
"/komp_ac.table_definition.TableDefinition/GetProfileTree" => { "/komp_ac.table_validation.TableValidationService/UpdateFieldValidation" => {
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
struct GetProfileTreeSvc<T: TableDefinition>(pub Arc<T>); struct UpdateFieldValidationSvc<T: TableValidationService>(
pub Arc<T>,
);
impl< impl<
T: TableDefinition, T: TableValidationService,
> tonic::server::UnaryService<super::super::common::Empty> > tonic::server::UnaryService<super::UpdateFieldValidationRequest>
for GetProfileTreeSvc<T> { for UpdateFieldValidationSvc<T> {
type Response = super::ProfileTreeResponse; type Response = super::UpdateFieldValidationResponse;
type Future = BoxFuture< type Future = BoxFuture<
tonic::Response<Self::Response>, tonic::Response<Self::Response>,
tonic::Status, tonic::Status,
>; >;
fn call( fn call(
&mut self, &mut self,
request: tonic::Request<super::super::common::Empty>, request: tonic::Request<super::UpdateFieldValidationRequest>,
) -> Self::Future { ) -> Self::Future {
let inner = Arc::clone(&self.0); let inner = Arc::clone(&self.0);
let fut = async move { let fut = async move {
<T as TableDefinition>::get_profile_tree(&inner, request) <T as TableValidationService>::update_field_validation(
&inner,
request,
)
.await .await
}; };
Box::pin(fut) Box::pin(fut)
@@ -441,52 +465,7 @@ pub mod table_definition_server {
let max_encoding_message_size = self.max_encoding_message_size; let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone(); let inner = self.inner.clone();
let fut = async move { let fut = async move {
let method = GetProfileTreeSvc(inner); let method = UpdateFieldValidationSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/komp_ac.table_definition.TableDefinition/DeleteTable" => {
#[allow(non_camel_case_types)]
struct DeleteTableSvc<T: TableDefinition>(pub Arc<T>);
impl<
T: TableDefinition,
> tonic::server::UnaryService<super::DeleteTableRequest>
for DeleteTableSvc<T> {
type Response = super::DeleteTableResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::DeleteTableRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as TableDefinition>::delete_table(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = DeleteTableSvc(inner);
let codec = tonic::codec::ProstCodec::default(); let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec) let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config( .apply_compression_config(
@@ -524,7 +503,7 @@ pub mod table_definition_server {
} }
} }
} }
impl<T> Clone for TableDefinitionServer<T> { impl<T> Clone for TableValidationServiceServer<T> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
let inner = self.inner.clone(); let inner = self.inner.clone();
Self { Self {
@@ -537,8 +516,8 @@ pub mod table_definition_server {
} }
} }
/// Generated gRPC service name /// Generated gRPC service name
pub const SERVICE_NAME: &str = "komp_ac.table_definition.TableDefinition"; pub const SERVICE_NAME: &str = "komp_ac.table_validation.TableValidationService";
impl<T> tonic::server::NamedService for TableDefinitionServer<T> { impl<T> tonic::server::NamedService for TableValidationServiceServer<T> {
const NAME: &'static str = SERVICE_NAME; const NAME: &'static str = SERVICE_NAME;
} }
} }

View File

@@ -1,791 +0,0 @@
// This file is @generated by prost-build.
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct GetAdresarRequest {
#[prost(int64, tag = "1")]
pub id: i64,
}
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct DeleteAdresarRequest {
#[prost(int64, tag = "1")]
pub id: i64,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PostAdresarRequest {
#[prost(string, tag = "1")]
pub firma: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub kz: ::prost::alloc::string::String,
#[prost(string, tag = "3")]
pub drc: ::prost::alloc::string::String,
#[prost(string, tag = "4")]
pub ulica: ::prost::alloc::string::String,
#[prost(string, tag = "5")]
pub psc: ::prost::alloc::string::String,
#[prost(string, tag = "6")]
pub mesto: ::prost::alloc::string::String,
#[prost(string, tag = "7")]
pub stat: ::prost::alloc::string::String,
#[prost(string, tag = "8")]
pub banka: ::prost::alloc::string::String,
#[prost(string, tag = "9")]
pub ucet: ::prost::alloc::string::String,
#[prost(string, tag = "10")]
pub skladm: ::prost::alloc::string::String,
#[prost(string, tag = "11")]
pub ico: ::prost::alloc::string::String,
#[prost(string, tag = "12")]
pub kontakt: ::prost::alloc::string::String,
#[prost(string, tag = "13")]
pub telefon: ::prost::alloc::string::String,
#[prost(string, tag = "14")]
pub skladu: ::prost::alloc::string::String,
#[prost(string, tag = "15")]
pub fax: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct AdresarResponse {
#[prost(int64, tag = "1")]
pub id: i64,
#[prost(string, tag = "2")]
pub firma: ::prost::alloc::string::String,
#[prost(string, tag = "3")]
pub kz: ::prost::alloc::string::String,
#[prost(string, tag = "4")]
pub drc: ::prost::alloc::string::String,
#[prost(string, tag = "5")]
pub ulica: ::prost::alloc::string::String,
#[prost(string, tag = "6")]
pub psc: ::prost::alloc::string::String,
#[prost(string, tag = "7")]
pub mesto: ::prost::alloc::string::String,
#[prost(string, tag = "8")]
pub stat: ::prost::alloc::string::String,
#[prost(string, tag = "9")]
pub banka: ::prost::alloc::string::String,
#[prost(string, tag = "10")]
pub ucet: ::prost::alloc::string::String,
#[prost(string, tag = "11")]
pub skladm: ::prost::alloc::string::String,
#[prost(string, tag = "12")]
pub ico: ::prost::alloc::string::String,
#[prost(string, tag = "13")]
pub kontakt: ::prost::alloc::string::String,
#[prost(string, tag = "14")]
pub telefon: ::prost::alloc::string::String,
#[prost(string, tag = "15")]
pub skladu: ::prost::alloc::string::String,
#[prost(string, tag = "16")]
pub fax: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PutAdresarRequest {
#[prost(int64, tag = "1")]
pub id: i64,
#[prost(string, tag = "2")]
pub firma: ::prost::alloc::string::String,
#[prost(string, tag = "3")]
pub kz: ::prost::alloc::string::String,
#[prost(string, tag = "4")]
pub drc: ::prost::alloc::string::String,
#[prost(string, tag = "5")]
pub ulica: ::prost::alloc::string::String,
#[prost(string, tag = "6")]
pub psc: ::prost::alloc::string::String,
#[prost(string, tag = "7")]
pub mesto: ::prost::alloc::string::String,
#[prost(string, tag = "8")]
pub stat: ::prost::alloc::string::String,
#[prost(string, tag = "9")]
pub banka: ::prost::alloc::string::String,
#[prost(string, tag = "10")]
pub ucet: ::prost::alloc::string::String,
#[prost(string, tag = "11")]
pub skladm: ::prost::alloc::string::String,
#[prost(string, tag = "12")]
pub ico: ::prost::alloc::string::String,
#[prost(string, tag = "13")]
pub kontakt: ::prost::alloc::string::String,
#[prost(string, tag = "14")]
pub telefon: ::prost::alloc::string::String,
#[prost(string, tag = "15")]
pub skladu: ::prost::alloc::string::String,
#[prost(string, tag = "16")]
pub fax: ::prost::alloc::string::String,
}
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct DeleteAdresarResponse {
#[prost(bool, tag = "1")]
pub success: bool,
}
/// Generated client implementations.
pub mod adresar_client {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
use tonic::codegen::http::Uri;
#[derive(Debug, Clone)]
pub struct AdresarClient<T> {
inner: tonic::client::Grpc<T>,
}
impl AdresarClient<tonic::transport::Channel> {
/// Attempt to create a new client by connecting to a given endpoint.
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
where
D: TryInto<tonic::transport::Endpoint>,
D::Error: Into<StdError>,
{
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
Ok(Self::new(conn))
}
}
impl<T> AdresarClient<T>
where
T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
{
pub fn new(inner: T) -> Self {
let inner = tonic::client::Grpc::new(inner);
Self { inner }
}
pub fn with_origin(inner: T, origin: Uri) -> Self {
let inner = tonic::client::Grpc::with_origin(inner, origin);
Self { inner }
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> AdresarClient<InterceptedService<T, F>>
where
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::Body>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::Body>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
AdresarClient::new(InterceptedService::new(inner, interceptor))
}
/// Compress requests with the given encoding.
///
/// This requires the server to support it otherwise it might respond with an
/// error.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.send_compressed(encoding);
self
}
/// Enable decompressing responses.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.accept_compressed(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_decoding_message_size(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_encoding_message_size(limit);
self
}
pub async fn post_adresar(
&mut self,
request: impl tonic::IntoRequest<super::PostAdresarRequest>,
) -> std::result::Result<
tonic::Response<super::AdresarResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.adresar.Adresar/PostAdresar",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "PostAdresar"));
self.inner.unary(req, path, codec).await
}
pub async fn get_adresar(
&mut self,
request: impl tonic::IntoRequest<super::GetAdresarRequest>,
) -> std::result::Result<
tonic::Response<super::AdresarResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.adresar.Adresar/GetAdresar",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "GetAdresar"));
self.inner.unary(req, path, codec).await
}
pub async fn put_adresar(
&mut self,
request: impl tonic::IntoRequest<super::PutAdresarRequest>,
) -> std::result::Result<
tonic::Response<super::AdresarResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.adresar.Adresar/PutAdresar",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "PutAdresar"));
self.inner.unary(req, path, codec).await
}
pub async fn delete_adresar(
&mut self,
request: impl tonic::IntoRequest<super::DeleteAdresarRequest>,
) -> std::result::Result<
tonic::Response<super::DeleteAdresarResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.adresar.Adresar/DeleteAdresar",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "DeleteAdresar"));
self.inner.unary(req, path, codec).await
}
pub async fn get_adresar_count(
&mut self,
request: impl tonic::IntoRequest<super::super::common::Empty>,
) -> std::result::Result<
tonic::Response<super::super::common::CountResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.adresar.Adresar/GetAdresarCount",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "GetAdresarCount"));
self.inner.unary(req, path, codec).await
}
pub async fn get_adresar_by_position(
&mut self,
request: impl tonic::IntoRequest<super::super::common::PositionRequest>,
) -> std::result::Result<
tonic::Response<super::AdresarResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.adresar.Adresar/GetAdresarByPosition",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new("komp_ac.adresar.Adresar", "GetAdresarByPosition"),
);
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
pub mod adresar_server {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
/// Generated trait containing gRPC methods that should be implemented for use with AdresarServer.
#[async_trait]
pub trait Adresar: std::marker::Send + std::marker::Sync + 'static {
async fn post_adresar(
&self,
request: tonic::Request<super::PostAdresarRequest>,
) -> std::result::Result<tonic::Response<super::AdresarResponse>, tonic::Status>;
async fn get_adresar(
&self,
request: tonic::Request<super::GetAdresarRequest>,
) -> std::result::Result<tonic::Response<super::AdresarResponse>, tonic::Status>;
async fn put_adresar(
&self,
request: tonic::Request<super::PutAdresarRequest>,
) -> std::result::Result<tonic::Response<super::AdresarResponse>, tonic::Status>;
async fn delete_adresar(
&self,
request: tonic::Request<super::DeleteAdresarRequest>,
) -> std::result::Result<
tonic::Response<super::DeleteAdresarResponse>,
tonic::Status,
>;
async fn get_adresar_count(
&self,
request: tonic::Request<super::super::common::Empty>,
) -> std::result::Result<
tonic::Response<super::super::common::CountResponse>,
tonic::Status,
>;
async fn get_adresar_by_position(
&self,
request: tonic::Request<super::super::common::PositionRequest>,
) -> std::result::Result<tonic::Response<super::AdresarResponse>, tonic::Status>;
}
#[derive(Debug)]
pub struct AdresarServer<T> {
inner: Arc<T>,
accept_compression_encodings: EnabledCompressionEncodings,
send_compression_encodings: EnabledCompressionEncodings,
max_decoding_message_size: Option<usize>,
max_encoding_message_size: Option<usize>,
}
impl<T> AdresarServer<T> {
pub fn new(inner: T) -> Self {
Self::from_arc(Arc::new(inner))
}
pub fn from_arc(inner: Arc<T>) -> Self {
Self {
inner,
accept_compression_encodings: Default::default(),
send_compression_encodings: Default::default(),
max_decoding_message_size: None,
max_encoding_message_size: None,
}
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> InterceptedService<Self, F>
where
F: tonic::service::Interceptor,
{
InterceptedService::new(Self::new(inner), interceptor)
}
/// Enable decompressing requests with the given encoding.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.accept_compression_encodings.enable(encoding);
self
}
/// Compress responses with the given encoding, if the client supports it.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.send_compression_encodings.enable(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.max_decoding_message_size = Some(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.max_encoding_message_size = Some(limit);
self
}
}
impl<T, B> tonic::codegen::Service<http::Request<B>> for AdresarServer<T>
where
T: Adresar,
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::Body>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/komp_ac.adresar.Adresar/PostAdresar" => {
#[allow(non_camel_case_types)]
struct PostAdresarSvc<T: Adresar>(pub Arc<T>);
impl<
T: Adresar,
> tonic::server::UnaryService<super::PostAdresarRequest>
for PostAdresarSvc<T> {
type Response = super::AdresarResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::PostAdresarRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Adresar>::post_adresar(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = PostAdresarSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/komp_ac.adresar.Adresar/GetAdresar" => {
#[allow(non_camel_case_types)]
struct GetAdresarSvc<T: Adresar>(pub Arc<T>);
impl<
T: Adresar,
> tonic::server::UnaryService<super::GetAdresarRequest>
for GetAdresarSvc<T> {
type Response = super::AdresarResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::GetAdresarRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Adresar>::get_adresar(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = GetAdresarSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/komp_ac.adresar.Adresar/PutAdresar" => {
#[allow(non_camel_case_types)]
struct PutAdresarSvc<T: Adresar>(pub Arc<T>);
impl<
T: Adresar,
> tonic::server::UnaryService<super::PutAdresarRequest>
for PutAdresarSvc<T> {
type Response = super::AdresarResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::PutAdresarRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Adresar>::put_adresar(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = PutAdresarSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/komp_ac.adresar.Adresar/DeleteAdresar" => {
#[allow(non_camel_case_types)]
struct DeleteAdresarSvc<T: Adresar>(pub Arc<T>);
impl<
T: Adresar,
> tonic::server::UnaryService<super::DeleteAdresarRequest>
for DeleteAdresarSvc<T> {
type Response = super::DeleteAdresarResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::DeleteAdresarRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Adresar>::delete_adresar(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = DeleteAdresarSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/komp_ac.adresar.Adresar/GetAdresarCount" => {
#[allow(non_camel_case_types)]
struct GetAdresarCountSvc<T: Adresar>(pub Arc<T>);
impl<
T: Adresar,
> tonic::server::UnaryService<super::super::common::Empty>
for GetAdresarCountSvc<T> {
type Response = super::super::common::CountResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::super::common::Empty>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Adresar>::get_adresar_count(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = GetAdresarCountSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/komp_ac.adresar.Adresar/GetAdresarByPosition" => {
#[allow(non_camel_case_types)]
struct GetAdresarByPositionSvc<T: Adresar>(pub Arc<T>);
impl<
T: Adresar,
> tonic::server::UnaryService<super::super::common::PositionRequest>
for GetAdresarByPositionSvc<T> {
type Response = super::AdresarResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<
super::super::common::PositionRequest,
>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Adresar>::get_adresar_by_position(&inner, request)
.await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = GetAdresarByPositionSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(
tonic::body::Body::default(),
);
let headers = response.headers_mut();
headers
.insert(
tonic::Status::GRPC_STATUS,
(tonic::Code::Unimplemented as i32).into(),
);
headers
.insert(
http::header::CONTENT_TYPE,
tonic::metadata::GRPC_CONTENT_TYPE,
);
Ok(response)
})
}
}
}
}
impl<T> Clone for AdresarServer<T> {
fn clone(&self) -> Self {
let inner = self.inner.clone();
Self {
inner,
accept_compression_encodings: self.accept_compression_encodings,
send_compression_encodings: self.send_compression_encodings,
max_decoding_message_size: self.max_decoding_message_size,
max_encoding_message_size: self.max_encoding_message_size,
}
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "komp_ac.adresar.Adresar";
impl<T> tonic::server::NamedService for AdresarServer<T> {
const NAME: &'static str = SERVICE_NAME;
}
}

View File

@@ -1,418 +0,0 @@
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct RegisterRequest {
#[prost(string, tag = "1")]
pub username: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub email: ::prost::alloc::string::String,
#[prost(string, tag = "3")]
pub password: ::prost::alloc::string::String,
#[prost(string, tag = "4")]
pub password_confirmation: ::prost::alloc::string::String,
#[prost(string, tag = "5")]
pub role: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct AuthResponse {
/// UUID in string format
#[prost(string, tag = "1")]
pub id: ::prost::alloc::string::String,
/// Registered username
#[prost(string, tag = "2")]
pub username: ::prost::alloc::string::String,
/// Registered email (if provided)
#[prost(string, tag = "3")]
pub email: ::prost::alloc::string::String,
/// Default role: 'accountant'
#[prost(string, tag = "4")]
pub role: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct LoginRequest {
/// Can be username or email
#[prost(string, tag = "1")]
pub identifier: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub password: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct LoginResponse {
/// JWT token
#[prost(string, tag = "1")]
pub access_token: ::prost::alloc::string::String,
/// Usually "Bearer"
#[prost(string, tag = "2")]
pub token_type: ::prost::alloc::string::String,
/// Expiration in seconds (86400 for 24 hours)
#[prost(int32, tag = "3")]
pub expires_in: i32,
/// User's UUID in string format
#[prost(string, tag = "4")]
pub user_id: ::prost::alloc::string::String,
/// User's role
#[prost(string, tag = "5")]
pub role: ::prost::alloc::string::String,
#[prost(string, tag = "6")]
pub username: ::prost::alloc::string::String,
}
/// Generated client implementations.
pub mod auth_service_client {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
use tonic::codegen::http::Uri;
#[derive(Debug, Clone)]
pub struct AuthServiceClient<T> {
inner: tonic::client::Grpc<T>,
}
impl AuthServiceClient<tonic::transport::Channel> {
/// Attempt to create a new client by connecting to a given endpoint.
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
where
D: TryInto<tonic::transport::Endpoint>,
D::Error: Into<StdError>,
{
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
Ok(Self::new(conn))
}
}
impl<T> AuthServiceClient<T>
where
T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
{
pub fn new(inner: T) -> Self {
let inner = tonic::client::Grpc::new(inner);
Self { inner }
}
pub fn with_origin(inner: T, origin: Uri) -> Self {
let inner = tonic::client::Grpc::with_origin(inner, origin);
Self { inner }
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> AuthServiceClient<InterceptedService<T, F>>
where
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::Body>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::Body>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
AuthServiceClient::new(InterceptedService::new(inner, interceptor))
}
/// Compress requests with the given encoding.
///
/// This requires the server to support it otherwise it might respond with an
/// error.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.send_compressed(encoding);
self
}
/// Enable decompressing responses.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.accept_compressed(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_decoding_message_size(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_encoding_message_size(limit);
self
}
pub async fn register(
&mut self,
request: impl tonic::IntoRequest<super::RegisterRequest>,
) -> std::result::Result<tonic::Response<super::AuthResponse>, tonic::Status> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.auth.AuthService/Register",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("komp_ac.auth.AuthService", "Register"));
self.inner.unary(req, path, codec).await
}
pub async fn login(
&mut self,
request: impl tonic::IntoRequest<super::LoginRequest>,
) -> std::result::Result<tonic::Response<super::LoginResponse>, tonic::Status> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.auth.AuthService/Login",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("komp_ac.auth.AuthService", "Login"));
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
pub mod auth_service_server {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
/// Generated trait containing gRPC methods that should be implemented for use with AuthServiceServer.
#[async_trait]
pub trait AuthService: std::marker::Send + std::marker::Sync + 'static {
async fn register(
&self,
request: tonic::Request<super::RegisterRequest>,
) -> std::result::Result<tonic::Response<super::AuthResponse>, tonic::Status>;
async fn login(
&self,
request: tonic::Request<super::LoginRequest>,
) -> std::result::Result<tonic::Response<super::LoginResponse>, tonic::Status>;
}
#[derive(Debug)]
pub struct AuthServiceServer<T> {
inner: Arc<T>,
accept_compression_encodings: EnabledCompressionEncodings,
send_compression_encodings: EnabledCompressionEncodings,
max_decoding_message_size: Option<usize>,
max_encoding_message_size: Option<usize>,
}
impl<T> AuthServiceServer<T> {
pub fn new(inner: T) -> Self {
Self::from_arc(Arc::new(inner))
}
pub fn from_arc(inner: Arc<T>) -> Self {
Self {
inner,
accept_compression_encodings: Default::default(),
send_compression_encodings: Default::default(),
max_decoding_message_size: None,
max_encoding_message_size: None,
}
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> InterceptedService<Self, F>
where
F: tonic::service::Interceptor,
{
InterceptedService::new(Self::new(inner), interceptor)
}
/// Enable decompressing requests with the given encoding.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.accept_compression_encodings.enable(encoding);
self
}
/// Compress responses with the given encoding, if the client supports it.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.send_compression_encodings.enable(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.max_decoding_message_size = Some(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.max_encoding_message_size = Some(limit);
self
}
}
impl<T, B> tonic::codegen::Service<http::Request<B>> for AuthServiceServer<T>
where
T: AuthService,
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::Body>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/komp_ac.auth.AuthService/Register" => {
#[allow(non_camel_case_types)]
struct RegisterSvc<T: AuthService>(pub Arc<T>);
impl<
T: AuthService,
> tonic::server::UnaryService<super::RegisterRequest>
for RegisterSvc<T> {
type Response = super::AuthResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::RegisterRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as AuthService>::register(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = RegisterSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/komp_ac.auth.AuthService/Login" => {
#[allow(non_camel_case_types)]
struct LoginSvc<T: AuthService>(pub Arc<T>);
impl<T: AuthService> tonic::server::UnaryService<super::LoginRequest>
for LoginSvc<T> {
type Response = super::LoginResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::LoginRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as AuthService>::login(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = LoginSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(
tonic::body::Body::default(),
);
let headers = response.headers_mut();
headers
.insert(
tonic::Status::GRPC_STATUS,
(tonic::Code::Unimplemented as i32).into(),
);
headers
.insert(
http::header::CONTENT_TYPE,
tonic::metadata::GRPC_CONTENT_TYPE,
);
Ok(response)
})
}
}
}
}
impl<T> Clone for AuthServiceServer<T> {
fn clone(&self) -> Self {
let inner = self.inner.clone();
Self {
inner,
accept_compression_encodings: self.accept_compression_encodings,
send_compression_encodings: self.send_compression_encodings,
max_decoding_message_size: self.max_decoding_message_size,
max_encoding_message_size: self.max_encoding_message_size,
}
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "komp_ac.auth.AuthService";
impl<T> tonic::server::NamedService for AuthServiceServer<T> {
const NAME: &'static str = SERVICE_NAME;
}
}

View File

@@ -1,13 +0,0 @@
// This file is @generated by prost-build.
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct Empty {}
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct CountResponse {
#[prost(int64, tag = "1")]
pub count: i64,
}
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct PositionRequest {
#[prost(int64, tag = "1")]
pub position: i64,
}

View File

@@ -1,317 +0,0 @@
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SearchRequest {
#[prost(string, tag = "1")]
pub table_name: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub query: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SearchResponse {
#[prost(message, repeated, tag = "1")]
pub hits: ::prost::alloc::vec::Vec<search_response::Hit>,
}
/// Nested message and enum types in `SearchResponse`.
pub mod search_response {
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Hit {
/// PostgreSQL row ID
#[prost(int64, tag = "1")]
pub id: i64,
#[prost(float, tag = "2")]
pub score: f32,
#[prost(string, tag = "3")]
pub content_json: ::prost::alloc::string::String,
}
}
/// Generated client implementations.
pub mod searcher_client {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
use tonic::codegen::http::Uri;
#[derive(Debug, Clone)]
pub struct SearcherClient<T> {
inner: tonic::client::Grpc<T>,
}
impl SearcherClient<tonic::transport::Channel> {
/// Attempt to create a new client by connecting to a given endpoint.
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
where
D: TryInto<tonic::transport::Endpoint>,
D::Error: Into<StdError>,
{
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
Ok(Self::new(conn))
}
}
impl<T> SearcherClient<T>
where
T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
{
pub fn new(inner: T) -> Self {
let inner = tonic::client::Grpc::new(inner);
Self { inner }
}
pub fn with_origin(inner: T, origin: Uri) -> Self {
let inner = tonic::client::Grpc::with_origin(inner, origin);
Self { inner }
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> SearcherClient<InterceptedService<T, F>>
where
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::Body>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::Body>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
SearcherClient::new(InterceptedService::new(inner, interceptor))
}
/// Compress requests with the given encoding.
///
/// This requires the server to support it otherwise it might respond with an
/// error.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.send_compressed(encoding);
self
}
/// Enable decompressing responses.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.accept_compressed(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_decoding_message_size(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_encoding_message_size(limit);
self
}
pub async fn search_table(
&mut self,
request: impl tonic::IntoRequest<super::SearchRequest>,
) -> std::result::Result<tonic::Response<super::SearchResponse>, tonic::Status> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.search.Searcher/SearchTable",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("komp_ac.search.Searcher", "SearchTable"));
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
pub mod searcher_server {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
/// Generated trait containing gRPC methods that should be implemented for use with SearcherServer.
#[async_trait]
pub trait Searcher: std::marker::Send + std::marker::Sync + 'static {
async fn search_table(
&self,
request: tonic::Request<super::SearchRequest>,
) -> std::result::Result<tonic::Response<super::SearchResponse>, tonic::Status>;
}
#[derive(Debug)]
pub struct SearcherServer<T> {
inner: Arc<T>,
accept_compression_encodings: EnabledCompressionEncodings,
send_compression_encodings: EnabledCompressionEncodings,
max_decoding_message_size: Option<usize>,
max_encoding_message_size: Option<usize>,
}
impl<T> SearcherServer<T> {
pub fn new(inner: T) -> Self {
Self::from_arc(Arc::new(inner))
}
pub fn from_arc(inner: Arc<T>) -> Self {
Self {
inner,
accept_compression_encodings: Default::default(),
send_compression_encodings: Default::default(),
max_decoding_message_size: None,
max_encoding_message_size: None,
}
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> InterceptedService<Self, F>
where
F: tonic::service::Interceptor,
{
InterceptedService::new(Self::new(inner), interceptor)
}
/// Enable decompressing requests with the given encoding.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.accept_compression_encodings.enable(encoding);
self
}
/// Compress responses with the given encoding, if the client supports it.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.send_compression_encodings.enable(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.max_decoding_message_size = Some(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.max_encoding_message_size = Some(limit);
self
}
}
impl<T, B> tonic::codegen::Service<http::Request<B>> for SearcherServer<T>
where
T: Searcher,
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::Body>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/komp_ac.search.Searcher/SearchTable" => {
#[allow(non_camel_case_types)]
struct SearchTableSvc<T: Searcher>(pub Arc<T>);
impl<T: Searcher> tonic::server::UnaryService<super::SearchRequest>
for SearchTableSvc<T> {
type Response = super::SearchResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::SearchRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Searcher>::search_table(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = SearchTableSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(
tonic::body::Body::default(),
);
let headers = response.headers_mut();
headers
.insert(
tonic::Status::GRPC_STATUS,
(tonic::Code::Unimplemented as i32).into(),
);
headers
.insert(
http::header::CONTENT_TYPE,
tonic::metadata::GRPC_CONTENT_TYPE,
);
Ok(response)
})
}
}
}
}
impl<T> Clone for SearcherServer<T> {
fn clone(&self) -> Self {
let inner = self.inner.clone();
Self {
inner,
accept_compression_encodings: self.accept_compression_encodings,
send_compression_encodings: self.send_compression_encodings,
max_decoding_message_size: self.max_decoding_message_size,
max_encoding_message_size: self.max_encoding_message_size,
}
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "komp_ac.search.Searcher";
impl<T> tonic::server::NamedService for SearcherServer<T> {
const NAME: &'static str = SERVICE_NAME;
}
}

View File

@@ -1,323 +0,0 @@
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PostTableScriptRequest {
#[prost(int64, tag = "1")]
pub table_definition_id: i64,
#[prost(string, tag = "2")]
pub target_column: ::prost::alloc::string::String,
#[prost(string, tag = "3")]
pub script: ::prost::alloc::string::String,
#[prost(string, tag = "4")]
pub description: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TableScriptResponse {
#[prost(int64, tag = "1")]
pub id: i64,
#[prost(string, tag = "2")]
pub warnings: ::prost::alloc::string::String,
}
/// Generated client implementations.
pub mod table_script_client {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
use tonic::codegen::http::Uri;
#[derive(Debug, Clone)]
pub struct TableScriptClient<T> {
inner: tonic::client::Grpc<T>,
}
impl TableScriptClient<tonic::transport::Channel> {
/// Attempt to create a new client by connecting to a given endpoint.
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
where
D: TryInto<tonic::transport::Endpoint>,
D::Error: Into<StdError>,
{
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
Ok(Self::new(conn))
}
}
impl<T> TableScriptClient<T>
where
T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
{
pub fn new(inner: T) -> Self {
let inner = tonic::client::Grpc::new(inner);
Self { inner }
}
pub fn with_origin(inner: T, origin: Uri) -> Self {
let inner = tonic::client::Grpc::with_origin(inner, origin);
Self { inner }
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> TableScriptClient<InterceptedService<T, F>>
where
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::Body>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::Body>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
TableScriptClient::new(InterceptedService::new(inner, interceptor))
}
/// Compress requests with the given encoding.
///
/// This requires the server to support it otherwise it might respond with an
/// error.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.send_compressed(encoding);
self
}
/// Enable decompressing responses.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.accept_compressed(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_decoding_message_size(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_encoding_message_size(limit);
self
}
pub async fn post_table_script(
&mut self,
request: impl tonic::IntoRequest<super::PostTableScriptRequest>,
) -> std::result::Result<
tonic::Response<super::TableScriptResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_script.TableScript/PostTableScript",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"komp_ac.table_script.TableScript",
"PostTableScript",
),
);
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
pub mod table_script_server {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
/// Generated trait containing gRPC methods that should be implemented for use with TableScriptServer.
#[async_trait]
pub trait TableScript: std::marker::Send + std::marker::Sync + 'static {
async fn post_table_script(
&self,
request: tonic::Request<super::PostTableScriptRequest>,
) -> std::result::Result<
tonic::Response<super::TableScriptResponse>,
tonic::Status,
>;
}
#[derive(Debug)]
pub struct TableScriptServer<T> {
inner: Arc<T>,
accept_compression_encodings: EnabledCompressionEncodings,
send_compression_encodings: EnabledCompressionEncodings,
max_decoding_message_size: Option<usize>,
max_encoding_message_size: Option<usize>,
}
impl<T> TableScriptServer<T> {
pub fn new(inner: T) -> Self {
Self::from_arc(Arc::new(inner))
}
pub fn from_arc(inner: Arc<T>) -> Self {
Self {
inner,
accept_compression_encodings: Default::default(),
send_compression_encodings: Default::default(),
max_decoding_message_size: None,
max_encoding_message_size: None,
}
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> InterceptedService<Self, F>
where
F: tonic::service::Interceptor,
{
InterceptedService::new(Self::new(inner), interceptor)
}
/// Enable decompressing requests with the given encoding.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.accept_compression_encodings.enable(encoding);
self
}
/// Compress responses with the given encoding, if the client supports it.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.send_compression_encodings.enable(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.max_decoding_message_size = Some(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.max_encoding_message_size = Some(limit);
self
}
}
impl<T, B> tonic::codegen::Service<http::Request<B>> for TableScriptServer<T>
where
T: TableScript,
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::Body>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/komp_ac.table_script.TableScript/PostTableScript" => {
#[allow(non_camel_case_types)]
struct PostTableScriptSvc<T: TableScript>(pub Arc<T>);
impl<
T: TableScript,
> tonic::server::UnaryService<super::PostTableScriptRequest>
for PostTableScriptSvc<T> {
type Response = super::TableScriptResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::PostTableScriptRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as TableScript>::post_table_script(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = PostTableScriptSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(
tonic::body::Body::default(),
);
let headers = response.headers_mut();
headers
.insert(
tonic::Status::GRPC_STATUS,
(tonic::Code::Unimplemented as i32).into(),
);
headers
.insert(
http::header::CONTENT_TYPE,
tonic::metadata::GRPC_CONTENT_TYPE,
);
Ok(response)
})
}
}
}
}
impl<T> Clone for TableScriptServer<T> {
fn clone(&self) -> Self {
let inner = self.inner.clone();
Self {
inner,
accept_compression_encodings: self.accept_compression_encodings,
send_compression_encodings: self.send_compression_encodings,
max_decoding_message_size: self.max_decoding_message_size,
max_encoding_message_size: self.max_encoding_message_size,
}
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "komp_ac.table_script.TableScript";
impl<T> tonic::server::NamedService for TableScriptServer<T> {
const NAME: &'static str = SERVICE_NAME;
}
}

View File

@@ -1,336 +0,0 @@
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GetTableStructureRequest {
/// e.g., "default"
#[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String,
/// e.g., "2025_adresar6"
#[prost(string, tag = "2")]
pub table_name: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TableStructureResponse {
#[prost(message, repeated, tag = "1")]
pub columns: ::prost::alloc::vec::Vec<TableColumn>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TableColumn {
#[prost(string, tag = "1")]
pub name: ::prost::alloc::string::String,
/// e.g., "TEXT", "BIGINT", "VARCHAR(255)", "TIMESTAMPTZ"
#[prost(string, tag = "2")]
pub data_type: ::prost::alloc::string::String,
#[prost(bool, tag = "3")]
pub is_nullable: bool,
#[prost(bool, tag = "4")]
pub is_primary_key: bool,
}
/// Generated client implementations.
pub mod table_structure_service_client {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
use tonic::codegen::http::Uri;
#[derive(Debug, Clone)]
pub struct TableStructureServiceClient<T> {
inner: tonic::client::Grpc<T>,
}
impl TableStructureServiceClient<tonic::transport::Channel> {
/// Attempt to create a new client by connecting to a given endpoint.
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
where
D: TryInto<tonic::transport::Endpoint>,
D::Error: Into<StdError>,
{
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
Ok(Self::new(conn))
}
}
impl<T> TableStructureServiceClient<T>
where
T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
{
pub fn new(inner: T) -> Self {
let inner = tonic::client::Grpc::new(inner);
Self { inner }
}
pub fn with_origin(inner: T, origin: Uri) -> Self {
let inner = tonic::client::Grpc::with_origin(inner, origin);
Self { inner }
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> TableStructureServiceClient<InterceptedService<T, F>>
where
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::Body>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::Body>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
TableStructureServiceClient::new(InterceptedService::new(inner, interceptor))
}
/// Compress requests with the given encoding.
///
/// This requires the server to support it otherwise it might respond with an
/// error.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.send_compressed(encoding);
self
}
/// Enable decompressing responses.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.accept_compressed(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_decoding_message_size(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_encoding_message_size(limit);
self
}
pub async fn get_table_structure(
&mut self,
request: impl tonic::IntoRequest<super::GetTableStructureRequest>,
) -> std::result::Result<
tonic::Response<super::TableStructureResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_structure.TableStructureService/GetTableStructure",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"komp_ac.table_structure.TableStructureService",
"GetTableStructure",
),
);
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
pub mod table_structure_service_server {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
/// Generated trait containing gRPC methods that should be implemented for use with TableStructureServiceServer.
#[async_trait]
pub trait TableStructureService: std::marker::Send + std::marker::Sync + 'static {
async fn get_table_structure(
&self,
request: tonic::Request<super::GetTableStructureRequest>,
) -> std::result::Result<
tonic::Response<super::TableStructureResponse>,
tonic::Status,
>;
}
#[derive(Debug)]
pub struct TableStructureServiceServer<T> {
inner: Arc<T>,
accept_compression_encodings: EnabledCompressionEncodings,
send_compression_encodings: EnabledCompressionEncodings,
max_decoding_message_size: Option<usize>,
max_encoding_message_size: Option<usize>,
}
impl<T> TableStructureServiceServer<T> {
pub fn new(inner: T) -> Self {
Self::from_arc(Arc::new(inner))
}
pub fn from_arc(inner: Arc<T>) -> Self {
Self {
inner,
accept_compression_encodings: Default::default(),
send_compression_encodings: Default::default(),
max_decoding_message_size: None,
max_encoding_message_size: None,
}
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> InterceptedService<Self, F>
where
F: tonic::service::Interceptor,
{
InterceptedService::new(Self::new(inner), interceptor)
}
/// Enable decompressing requests with the given encoding.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.accept_compression_encodings.enable(encoding);
self
}
/// Compress responses with the given encoding, if the client supports it.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.send_compression_encodings.enable(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.max_decoding_message_size = Some(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.max_encoding_message_size = Some(limit);
self
}
}
impl<T, B> tonic::codegen::Service<http::Request<B>>
for TableStructureServiceServer<T>
where
T: TableStructureService,
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::Body>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/komp_ac.table_structure.TableStructureService/GetTableStructure" => {
#[allow(non_camel_case_types)]
struct GetTableStructureSvc<T: TableStructureService>(pub Arc<T>);
impl<
T: TableStructureService,
> tonic::server::UnaryService<super::GetTableStructureRequest>
for GetTableStructureSvc<T> {
type Response = super::TableStructureResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::GetTableStructureRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as TableStructureService>::get_table_structure(
&inner,
request,
)
.await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = GetTableStructureSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(
tonic::body::Body::default(),
);
let headers = response.headers_mut();
headers
.insert(
tonic::Status::GRPC_STATUS,
(tonic::Code::Unimplemented as i32).into(),
);
headers
.insert(
http::header::CONTENT_TYPE,
tonic::metadata::GRPC_CONTENT_TYPE,
);
Ok(response)
})
}
}
}
}
impl<T> Clone for TableStructureServiceServer<T> {
fn clone(&self) -> Self {
let inner = self.inner.clone();
Self {
inner,
accept_compression_encodings: self.accept_compression_encodings,
send_compression_encodings: self.send_compression_encodings,
max_decoding_message_size: self.max_decoding_message_size,
max_encoding_message_size: self.max_encoding_message_size,
}
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "komp_ac.table_structure.TableStructureService";
impl<T> tonic::server::NamedService for TableStructureServiceServer<T> {
const NAME: &'static str = SERVICE_NAME;
}
}

View File

@@ -1,797 +0,0 @@
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PostTableDataRequest {
#[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub table_name: ::prost::alloc::string::String,
#[prost(map = "string, message", tag = "3")]
pub data: ::std::collections::HashMap<
::prost::alloc::string::String,
::prost_types::Value,
>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PostTableDataResponse {
#[prost(bool, tag = "1")]
pub success: bool,
#[prost(string, tag = "2")]
pub message: ::prost::alloc::string::String,
#[prost(int64, tag = "3")]
pub inserted_id: i64,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PutTableDataRequest {
#[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub table_name: ::prost::alloc::string::String,
#[prost(int64, tag = "3")]
pub id: i64,
#[prost(map = "string, message", tag = "4")]
pub data: ::std::collections::HashMap<
::prost::alloc::string::String,
::prost_types::Value,
>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PutTableDataResponse {
#[prost(bool, tag = "1")]
pub success: bool,
#[prost(string, tag = "2")]
pub message: ::prost::alloc::string::String,
#[prost(int64, tag = "3")]
pub updated_id: i64,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct DeleteTableDataRequest {
#[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub table_name: ::prost::alloc::string::String,
#[prost(int64, tag = "3")]
pub record_id: i64,
}
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct DeleteTableDataResponse {
#[prost(bool, tag = "1")]
pub success: bool,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GetTableDataRequest {
#[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub table_name: ::prost::alloc::string::String,
#[prost(int64, tag = "3")]
pub id: i64,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GetTableDataResponse {
#[prost(map = "string, string", tag = "1")]
pub data: ::std::collections::HashMap<
::prost::alloc::string::String,
::prost::alloc::string::String,
>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GetTableDataCountRequest {
#[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub table_name: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GetTableDataByPositionRequest {
#[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub table_name: ::prost::alloc::string::String,
#[prost(int32, tag = "3")]
pub position: i32,
}
/// Generated client implementations.
pub mod tables_data_client {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
use tonic::codegen::http::Uri;
#[derive(Debug, Clone)]
pub struct TablesDataClient<T> {
inner: tonic::client::Grpc<T>,
}
impl TablesDataClient<tonic::transport::Channel> {
/// Attempt to create a new client by connecting to a given endpoint.
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
where
D: TryInto<tonic::transport::Endpoint>,
D::Error: Into<StdError>,
{
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
Ok(Self::new(conn))
}
}
impl<T> TablesDataClient<T>
where
T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
{
pub fn new(inner: T) -> Self {
let inner = tonic::client::Grpc::new(inner);
Self { inner }
}
pub fn with_origin(inner: T, origin: Uri) -> Self {
let inner = tonic::client::Grpc::with_origin(inner, origin);
Self { inner }
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> TablesDataClient<InterceptedService<T, F>>
where
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::Body>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::Body>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
TablesDataClient::new(InterceptedService::new(inner, interceptor))
}
/// Compress requests with the given encoding.
///
/// This requires the server to support it otherwise it might respond with an
/// error.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.send_compressed(encoding);
self
}
/// Enable decompressing responses.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.accept_compressed(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_decoding_message_size(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_encoding_message_size(limit);
self
}
pub async fn post_table_data(
&mut self,
request: impl tonic::IntoRequest<super::PostTableDataRequest>,
) -> std::result::Result<
tonic::Response<super::PostTableDataResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.tables_data.TablesData/PostTableData",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new("komp_ac.tables_data.TablesData", "PostTableData"),
);
self.inner.unary(req, path, codec).await
}
pub async fn put_table_data(
&mut self,
request: impl tonic::IntoRequest<super::PutTableDataRequest>,
) -> std::result::Result<
tonic::Response<super::PutTableDataResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.tables_data.TablesData/PutTableData",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new("komp_ac.tables_data.TablesData", "PutTableData"),
);
self.inner.unary(req, path, codec).await
}
pub async fn delete_table_data(
&mut self,
request: impl tonic::IntoRequest<super::DeleteTableDataRequest>,
) -> std::result::Result<
tonic::Response<super::DeleteTableDataResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.tables_data.TablesData/DeleteTableData",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"komp_ac.tables_data.TablesData",
"DeleteTableData",
),
);
self.inner.unary(req, path, codec).await
}
pub async fn get_table_data(
&mut self,
request: impl tonic::IntoRequest<super::GetTableDataRequest>,
) -> std::result::Result<
tonic::Response<super::GetTableDataResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.tables_data.TablesData/GetTableData",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new("komp_ac.tables_data.TablesData", "GetTableData"),
);
self.inner.unary(req, path, codec).await
}
pub async fn get_table_data_count(
&mut self,
request: impl tonic::IntoRequest<super::GetTableDataCountRequest>,
) -> std::result::Result<
tonic::Response<super::super::common::CountResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.tables_data.TablesData/GetTableDataCount",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"komp_ac.tables_data.TablesData",
"GetTableDataCount",
),
);
self.inner.unary(req, path, codec).await
}
pub async fn get_table_data_by_position(
&mut self,
request: impl tonic::IntoRequest<super::GetTableDataByPositionRequest>,
) -> std::result::Result<
tonic::Response<super::GetTableDataResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.tables_data.TablesData/GetTableDataByPosition",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"komp_ac.tables_data.TablesData",
"GetTableDataByPosition",
),
);
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
pub mod tables_data_server {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
/// Generated trait containing gRPC methods that should be implemented for use with TablesDataServer.
#[async_trait]
pub trait TablesData: std::marker::Send + std::marker::Sync + 'static {
async fn post_table_data(
&self,
request: tonic::Request<super::PostTableDataRequest>,
) -> std::result::Result<
tonic::Response<super::PostTableDataResponse>,
tonic::Status,
>;
async fn put_table_data(
&self,
request: tonic::Request<super::PutTableDataRequest>,
) -> std::result::Result<
tonic::Response<super::PutTableDataResponse>,
tonic::Status,
>;
async fn delete_table_data(
&self,
request: tonic::Request<super::DeleteTableDataRequest>,
) -> std::result::Result<
tonic::Response<super::DeleteTableDataResponse>,
tonic::Status,
>;
async fn get_table_data(
&self,
request: tonic::Request<super::GetTableDataRequest>,
) -> std::result::Result<
tonic::Response<super::GetTableDataResponse>,
tonic::Status,
>;
async fn get_table_data_count(
&self,
request: tonic::Request<super::GetTableDataCountRequest>,
) -> std::result::Result<
tonic::Response<super::super::common::CountResponse>,
tonic::Status,
>;
async fn get_table_data_by_position(
&self,
request: tonic::Request<super::GetTableDataByPositionRequest>,
) -> std::result::Result<
tonic::Response<super::GetTableDataResponse>,
tonic::Status,
>;
}
#[derive(Debug)]
pub struct TablesDataServer<T> {
inner: Arc<T>,
accept_compression_encodings: EnabledCompressionEncodings,
send_compression_encodings: EnabledCompressionEncodings,
max_decoding_message_size: Option<usize>,
max_encoding_message_size: Option<usize>,
}
impl<T> TablesDataServer<T> {
pub fn new(inner: T) -> Self {
Self::from_arc(Arc::new(inner))
}
pub fn from_arc(inner: Arc<T>) -> Self {
Self {
inner,
accept_compression_encodings: Default::default(),
send_compression_encodings: Default::default(),
max_decoding_message_size: None,
max_encoding_message_size: None,
}
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> InterceptedService<Self, F>
where
F: tonic::service::Interceptor,
{
InterceptedService::new(Self::new(inner), interceptor)
}
/// Enable decompressing requests with the given encoding.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.accept_compression_encodings.enable(encoding);
self
}
/// Compress responses with the given encoding, if the client supports it.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.send_compression_encodings.enable(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.max_decoding_message_size = Some(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.max_encoding_message_size = Some(limit);
self
}
}
impl<T, B> tonic::codegen::Service<http::Request<B>> for TablesDataServer<T>
where
T: TablesData,
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::Body>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/komp_ac.tables_data.TablesData/PostTableData" => {
#[allow(non_camel_case_types)]
struct PostTableDataSvc<T: TablesData>(pub Arc<T>);
impl<
T: TablesData,
> tonic::server::UnaryService<super::PostTableDataRequest>
for PostTableDataSvc<T> {
type Response = super::PostTableDataResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::PostTableDataRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as TablesData>::post_table_data(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = PostTableDataSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/komp_ac.tables_data.TablesData/PutTableData" => {
#[allow(non_camel_case_types)]
struct PutTableDataSvc<T: TablesData>(pub Arc<T>);
impl<
T: TablesData,
> tonic::server::UnaryService<super::PutTableDataRequest>
for PutTableDataSvc<T> {
type Response = super::PutTableDataResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::PutTableDataRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as TablesData>::put_table_data(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = PutTableDataSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/komp_ac.tables_data.TablesData/DeleteTableData" => {
#[allow(non_camel_case_types)]
struct DeleteTableDataSvc<T: TablesData>(pub Arc<T>);
impl<
T: TablesData,
> tonic::server::UnaryService<super::DeleteTableDataRequest>
for DeleteTableDataSvc<T> {
type Response = super::DeleteTableDataResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::DeleteTableDataRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as TablesData>::delete_table_data(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = DeleteTableDataSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/komp_ac.tables_data.TablesData/GetTableData" => {
#[allow(non_camel_case_types)]
struct GetTableDataSvc<T: TablesData>(pub Arc<T>);
impl<
T: TablesData,
> tonic::server::UnaryService<super::GetTableDataRequest>
for GetTableDataSvc<T> {
type Response = super::GetTableDataResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::GetTableDataRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as TablesData>::get_table_data(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = GetTableDataSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/komp_ac.tables_data.TablesData/GetTableDataCount" => {
#[allow(non_camel_case_types)]
struct GetTableDataCountSvc<T: TablesData>(pub Arc<T>);
impl<
T: TablesData,
> tonic::server::UnaryService<super::GetTableDataCountRequest>
for GetTableDataCountSvc<T> {
type Response = super::super::common::CountResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::GetTableDataCountRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as TablesData>::get_table_data_count(&inner, request)
.await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = GetTableDataCountSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/komp_ac.tables_data.TablesData/GetTableDataByPosition" => {
#[allow(non_camel_case_types)]
struct GetTableDataByPositionSvc<T: TablesData>(pub Arc<T>);
impl<
T: TablesData,
> tonic::server::UnaryService<super::GetTableDataByPositionRequest>
for GetTableDataByPositionSvc<T> {
type Response = super::GetTableDataResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::GetTableDataByPositionRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as TablesData>::get_table_data_by_position(
&inner,
request,
)
.await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = GetTableDataByPositionSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(
tonic::body::Body::default(),
);
let headers = response.headers_mut();
headers
.insert(
tonic::Status::GRPC_STATUS,
(tonic::Code::Unimplemented as i32).into(),
);
headers
.insert(
http::header::CONTENT_TYPE,
tonic::metadata::GRPC_CONTENT_TYPE,
);
Ok(response)
})
}
}
}
}
impl<T> Clone for TablesDataServer<T> {
fn clone(&self) -> Self {
let inner = self.inner.clone();
Self {
inner,
accept_compression_encodings: self.accept_compression_encodings,
send_compression_encodings: self.send_compression_encodings,
max_decoding_message_size: self.max_decoding_message_size,
max_encoding_message_size: self.max_encoding_message_size,
}
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "komp_ac.tables_data.TablesData";
impl<T> tonic::server::NamedService for TablesDataServer<T> {
const NAME: &'static str = SERVICE_NAME;
}
}

View File

@@ -1,721 +0,0 @@
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PostUctovnictvoRequest {
#[prost(int64, tag = "1")]
pub adresar_id: i64,
#[prost(string, tag = "2")]
pub c_dokladu: ::prost::alloc::string::String,
/// Use string for simplicity, or use google.protobuf.Timestamp for better date handling
#[prost(string, tag = "3")]
pub datum: ::prost::alloc::string::String,
#[prost(string, tag = "4")]
pub c_faktury: ::prost::alloc::string::String,
#[prost(string, tag = "5")]
pub obsah: ::prost::alloc::string::String,
#[prost(string, tag = "6")]
pub stredisko: ::prost::alloc::string::String,
#[prost(string, tag = "7")]
pub c_uctu: ::prost::alloc::string::String,
#[prost(string, tag = "8")]
pub md: ::prost::alloc::string::String,
#[prost(string, tag = "9")]
pub identif: ::prost::alloc::string::String,
#[prost(string, tag = "10")]
pub poznanka: ::prost::alloc::string::String,
#[prost(string, tag = "11")]
pub firma: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct UctovnictvoResponse {
#[prost(int64, tag = "1")]
pub id: i64,
#[prost(int64, tag = "2")]
pub adresar_id: i64,
#[prost(string, tag = "3")]
pub c_dokladu: ::prost::alloc::string::String,
#[prost(string, tag = "4")]
pub datum: ::prost::alloc::string::String,
#[prost(string, tag = "5")]
pub c_faktury: ::prost::alloc::string::String,
#[prost(string, tag = "6")]
pub obsah: ::prost::alloc::string::String,
#[prost(string, tag = "7")]
pub stredisko: ::prost::alloc::string::String,
#[prost(string, tag = "8")]
pub c_uctu: ::prost::alloc::string::String,
#[prost(string, tag = "9")]
pub md: ::prost::alloc::string::String,
#[prost(string, tag = "10")]
pub identif: ::prost::alloc::string::String,
#[prost(string, tag = "11")]
pub poznanka: ::prost::alloc::string::String,
#[prost(string, tag = "12")]
pub firma: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PutUctovnictvoRequest {
#[prost(int64, tag = "1")]
pub id: i64,
#[prost(int64, tag = "2")]
pub adresar_id: i64,
#[prost(string, tag = "3")]
pub c_dokladu: ::prost::alloc::string::String,
#[prost(string, tag = "4")]
pub datum: ::prost::alloc::string::String,
#[prost(string, tag = "5")]
pub c_faktury: ::prost::alloc::string::String,
#[prost(string, tag = "6")]
pub obsah: ::prost::alloc::string::String,
#[prost(string, tag = "7")]
pub stredisko: ::prost::alloc::string::String,
#[prost(string, tag = "8")]
pub c_uctu: ::prost::alloc::string::String,
#[prost(string, tag = "9")]
pub md: ::prost::alloc::string::String,
#[prost(string, tag = "10")]
pub identif: ::prost::alloc::string::String,
#[prost(string, tag = "11")]
pub poznanka: ::prost::alloc::string::String,
#[prost(string, tag = "12")]
pub firma: ::prost::alloc::string::String,
}
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct GetUctovnictvoRequest {
#[prost(int64, tag = "1")]
pub id: i64,
}
/// Generated client implementations.
pub mod uctovnictvo_client {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
use tonic::codegen::http::Uri;
#[derive(Debug, Clone)]
pub struct UctovnictvoClient<T> {
inner: tonic::client::Grpc<T>,
}
impl UctovnictvoClient<tonic::transport::Channel> {
/// Attempt to create a new client by connecting to a given endpoint.
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
where
D: TryInto<tonic::transport::Endpoint>,
D::Error: Into<StdError>,
{
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
Ok(Self::new(conn))
}
}
impl<T> UctovnictvoClient<T>
where
T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
{
pub fn new(inner: T) -> Self {
let inner = tonic::client::Grpc::new(inner);
Self { inner }
}
pub fn with_origin(inner: T, origin: Uri) -> Self {
let inner = tonic::client::Grpc::with_origin(inner, origin);
Self { inner }
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> UctovnictvoClient<InterceptedService<T, F>>
where
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::Body>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::Body>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
UctovnictvoClient::new(InterceptedService::new(inner, interceptor))
}
/// Compress requests with the given encoding.
///
/// This requires the server to support it otherwise it might respond with an
/// error.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.send_compressed(encoding);
self
}
/// Enable decompressing responses.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.accept_compressed(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_decoding_message_size(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_encoding_message_size(limit);
self
}
pub async fn post_uctovnictvo(
&mut self,
request: impl tonic::IntoRequest<super::PostUctovnictvoRequest>,
) -> std::result::Result<
tonic::Response<super::UctovnictvoResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.uctovnictvo.Uctovnictvo/PostUctovnictvo",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"komp_ac.uctovnictvo.Uctovnictvo",
"PostUctovnictvo",
),
);
self.inner.unary(req, path, codec).await
}
pub async fn get_uctovnictvo(
&mut self,
request: impl tonic::IntoRequest<super::GetUctovnictvoRequest>,
) -> std::result::Result<
tonic::Response<super::UctovnictvoResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvo",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"komp_ac.uctovnictvo.Uctovnictvo",
"GetUctovnictvo",
),
);
self.inner.unary(req, path, codec).await
}
pub async fn get_uctovnictvo_count(
&mut self,
request: impl tonic::IntoRequest<super::super::common::Empty>,
) -> std::result::Result<
tonic::Response<super::super::common::CountResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoCount",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"komp_ac.uctovnictvo.Uctovnictvo",
"GetUctovnictvoCount",
),
);
self.inner.unary(req, path, codec).await
}
pub async fn get_uctovnictvo_by_position(
&mut self,
request: impl tonic::IntoRequest<super::super::common::PositionRequest>,
) -> std::result::Result<
tonic::Response<super::UctovnictvoResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoByPosition",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"komp_ac.uctovnictvo.Uctovnictvo",
"GetUctovnictvoByPosition",
),
);
self.inner.unary(req, path, codec).await
}
pub async fn put_uctovnictvo(
&mut self,
request: impl tonic::IntoRequest<super::PutUctovnictvoRequest>,
) -> std::result::Result<
tonic::Response<super::UctovnictvoResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.uctovnictvo.Uctovnictvo/PutUctovnictvo",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"komp_ac.uctovnictvo.Uctovnictvo",
"PutUctovnictvo",
),
);
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
pub mod uctovnictvo_server {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
/// Generated trait containing gRPC methods that should be implemented for use with UctovnictvoServer.
#[async_trait]
pub trait Uctovnictvo: std::marker::Send + std::marker::Sync + 'static {
async fn post_uctovnictvo(
&self,
request: tonic::Request<super::PostUctovnictvoRequest>,
) -> std::result::Result<
tonic::Response<super::UctovnictvoResponse>,
tonic::Status,
>;
async fn get_uctovnictvo(
&self,
request: tonic::Request<super::GetUctovnictvoRequest>,
) -> std::result::Result<
tonic::Response<super::UctovnictvoResponse>,
tonic::Status,
>;
async fn get_uctovnictvo_count(
&self,
request: tonic::Request<super::super::common::Empty>,
) -> std::result::Result<
tonic::Response<super::super::common::CountResponse>,
tonic::Status,
>;
async fn get_uctovnictvo_by_position(
&self,
request: tonic::Request<super::super::common::PositionRequest>,
) -> std::result::Result<
tonic::Response<super::UctovnictvoResponse>,
tonic::Status,
>;
async fn put_uctovnictvo(
&self,
request: tonic::Request<super::PutUctovnictvoRequest>,
) -> std::result::Result<
tonic::Response<super::UctovnictvoResponse>,
tonic::Status,
>;
}
#[derive(Debug)]
pub struct UctovnictvoServer<T> {
inner: Arc<T>,
accept_compression_encodings: EnabledCompressionEncodings,
send_compression_encodings: EnabledCompressionEncodings,
max_decoding_message_size: Option<usize>,
max_encoding_message_size: Option<usize>,
}
impl<T> UctovnictvoServer<T> {
pub fn new(inner: T) -> Self {
Self::from_arc(Arc::new(inner))
}
pub fn from_arc(inner: Arc<T>) -> Self {
Self {
inner,
accept_compression_encodings: Default::default(),
send_compression_encodings: Default::default(),
max_decoding_message_size: None,
max_encoding_message_size: None,
}
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> InterceptedService<Self, F>
where
F: tonic::service::Interceptor,
{
InterceptedService::new(Self::new(inner), interceptor)
}
/// Enable decompressing requests with the given encoding.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.accept_compression_encodings.enable(encoding);
self
}
/// Compress responses with the given encoding, if the client supports it.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.send_compression_encodings.enable(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.max_decoding_message_size = Some(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.max_encoding_message_size = Some(limit);
self
}
}
impl<T, B> tonic::codegen::Service<http::Request<B>> for UctovnictvoServer<T>
where
T: Uctovnictvo,
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::Body>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/komp_ac.uctovnictvo.Uctovnictvo/PostUctovnictvo" => {
#[allow(non_camel_case_types)]
struct PostUctovnictvoSvc<T: Uctovnictvo>(pub Arc<T>);
impl<
T: Uctovnictvo,
> tonic::server::UnaryService<super::PostUctovnictvoRequest>
for PostUctovnictvoSvc<T> {
type Response = super::UctovnictvoResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::PostUctovnictvoRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Uctovnictvo>::post_uctovnictvo(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = PostUctovnictvoSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvo" => {
#[allow(non_camel_case_types)]
struct GetUctovnictvoSvc<T: Uctovnictvo>(pub Arc<T>);
impl<
T: Uctovnictvo,
> tonic::server::UnaryService<super::GetUctovnictvoRequest>
for GetUctovnictvoSvc<T> {
type Response = super::UctovnictvoResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::GetUctovnictvoRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Uctovnictvo>::get_uctovnictvo(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = GetUctovnictvoSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoCount" => {
#[allow(non_camel_case_types)]
struct GetUctovnictvoCountSvc<T: Uctovnictvo>(pub Arc<T>);
impl<
T: Uctovnictvo,
> tonic::server::UnaryService<super::super::common::Empty>
for GetUctovnictvoCountSvc<T> {
type Response = super::super::common::CountResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::super::common::Empty>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Uctovnictvo>::get_uctovnictvo_count(&inner, request)
.await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = GetUctovnictvoCountSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoByPosition" => {
#[allow(non_camel_case_types)]
struct GetUctovnictvoByPositionSvc<T: Uctovnictvo>(pub Arc<T>);
impl<
T: Uctovnictvo,
> tonic::server::UnaryService<super::super::common::PositionRequest>
for GetUctovnictvoByPositionSvc<T> {
type Response = super::UctovnictvoResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<
super::super::common::PositionRequest,
>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Uctovnictvo>::get_uctovnictvo_by_position(
&inner,
request,
)
.await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = GetUctovnictvoByPositionSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/komp_ac.uctovnictvo.Uctovnictvo/PutUctovnictvo" => {
#[allow(non_camel_case_types)]
struct PutUctovnictvoSvc<T: Uctovnictvo>(pub Arc<T>);
impl<
T: Uctovnictvo,
> tonic::server::UnaryService<super::PutUctovnictvoRequest>
for PutUctovnictvoSvc<T> {
type Response = super::UctovnictvoResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::PutUctovnictvoRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Uctovnictvo>::put_uctovnictvo(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = PutUctovnictvoSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(
tonic::body::Body::default(),
);
let headers = response.headers_mut();
headers
.insert(
tonic::Status::GRPC_STATUS,
(tonic::Code::Unimplemented as i32).into(),
);
headers
.insert(
http::header::CONTENT_TYPE,
tonic::metadata::GRPC_CONTENT_TYPE,
);
Ok(response)
})
}
}
}
}
impl<T> Clone for UctovnictvoServer<T> {
fn clone(&self) -> Self {
let inner = self.inner.clone();
Self {
inner,
accept_compression_encodings: self.accept_compression_encodings,
send_compression_encodings: self.send_compression_encodings,
max_decoding_message_size: self.max_decoding_message_size,
max_encoding_message_size: self.max_encoding_message_size,
}
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "komp_ac.uctovnictvo.Uctovnictvo";
impl<T> tonic::server::NamedService for UctovnictvoServer<T> {
const NAME: &'static str = SERVICE_NAME;
}
}

View File

@@ -10,10 +10,10 @@ search = { path = "../search" }
anyhow = { workspace = true } anyhow = { workspace = true }
tantivy = { workspace = true } tantivy = { workspace = true }
prost = "0.13.5"
prost-types = { workspace = true } prost-types = { workspace = true }
chrono = { version = "0.4.40", features = ["serde"] } chrono = { version = "0.4.40", features = ["serde"] }
dotenvy = "0.15.7" dotenvy = "0.15.7"
prost = "0.13.5"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140" serde_json = "1.0.140"
sqlx = { version = "0.8.5", features = ["chrono", "postgres", "runtime-tokio", "runtime-tokio-native-tls", "rust_decimal", "time", "uuid"] } sqlx = { version = "0.8.5", features = ["chrono", "postgres", "runtime-tokio", "runtime-tokio-native-tls", "rust_decimal", "time", "uuid"] }
@@ -40,6 +40,9 @@ regex = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
steel-decimal = "1.0.0" steel-decimal = "1.0.0"
[build-dependencies]
prost-build = "0.14.1"
[lib] [lib]
name = "server" name = "server"
path = "src/lib.rs" path = "src/lib.rs"

View File

@@ -0,0 +1,15 @@
-- Add migration script here
CREATE TABLE table_validation_rules (
id BIGSERIAL PRIMARY KEY,
table_def_id BIGINT NOT NULL
REFERENCES table_definitions(id)
ON DELETE CASCADE,
data_key TEXT NOT NULL,
config JSONB NOT NULL,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE (table_def_id, data_key)
);
CREATE INDEX idx_table_validation_rules_table
ON table_validation_rules (table_def_id);

View File

@@ -1,9 +1,39 @@
#!/bin/bash #!/bin/bash
# scripts/reset_test_db.sh # scripts/reset_test_db.sh
DATABASE_URL=${TEST_DATABASE_URL:-"postgres://multi_psql_dev:3@localhost:5432/multi_rust_test"} # Load environment variables from .env_test if it exists
if [ -f .env_test ]; then
export $(grep -v '^#' .env_test | xargs)
fi
echo "Reset db script" DATABASE_URL=${TEST_DATABASE_URL:-"postgres://multi_psql_dev:3@localhost:5432/multi_rust_test"}
yes | sqlx database drop --database-url "$DATABASE_URL" echo "Resetting test DB at $DATABASE_URL"
# Check if database exists and who owns it
DB_NAME=$(echo "$DATABASE_URL" | sed 's|.*/||')
DB_HOST=$(echo "$DATABASE_URL" | sed 's|.*@||' | sed 's|:.*||')
DB_PORT=$(echo "$DATABASE_URL" | sed 's|.*:||' | sed 's|/.*||')
DB_USER=$(echo "$DATABASE_URL" | sed 's|.*://||' | sed 's|:.*||')
# Check database owner (optional info)
DB_OWNER=$(psql "$DATABASE_URL" -t -c "SELECT pg_catalog.pg_get_userbyid(d.datdba) FROM pg_catalog.pg_database d WHERE d.datname = '$DB_NAME';" 2>/dev/null | xargs)
if [ ! -z "$DB_OWNER" ]; then
echo "Database owner: $DB_OWNER"
fi
# Force drop database without confirmation
echo "Dropping database..."
sqlx database drop --database-url "$DATABASE_URL" --yes 2>/dev/null || {
# Fallback: use psql to force drop
psql "postgres://$DB_USER@$DB_HOST:$DB_PORT/postgres" -c "DROP DATABASE IF EXISTS \"$DB_NAME\";" 2>/dev/null || true
}
# Create database
echo "Creating database..."
sqlx database create --database-url "$DATABASE_URL" sqlx database create --database-url "$DATABASE_URL"
echo "Test database reset complete."
# Apply migrations
echo "Applying migrations..."
sqlx migrate run --database-url "$DATABASE_URL"
echo "✅ Test database reset and migrated."

View File

@@ -10,6 +10,7 @@ pub mod table_definition;
pub mod tables_data; pub mod tables_data;
pub mod table_script; pub mod table_script;
pub mod steel; pub mod steel;
pub mod table_validation;
// Re-export run_server from the inner server module: // Re-export run_server from the inner server module:
pub use server::run_server; pub use server::run_server;

View File

@@ -16,12 +16,14 @@ use crate::server::services::{
use common::proto::komp_ac::{ use common::proto::komp_ac::{
table_structure::table_structure_service_server::TableStructureServiceServer, table_structure::table_structure_service_server::TableStructureServiceServer,
table_definition::table_definition_server::TableDefinitionServer, table_definition::table_definition_server::TableDefinitionServer,
table_validation::table_validation_service_server::TableValidationServiceServer,
tables_data::tables_data_server::TablesDataServer, tables_data::tables_data_server::TablesDataServer,
table_script::table_script_server::TableScriptServer, table_script::table_script_server::TableScriptServer,
auth::auth_service_server::AuthServiceServer, auth::auth_service_server::AuthServiceServer,
search2::search2_server::Search2Server, search2::search2_server::Search2Server,
}; };
use search::{SearcherService, SearcherServer}; use search::{SearcherService, SearcherServer};
use crate::table_validation::get::service::TableValidationSvc;
pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box<dyn std::error::Error>> { pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box<dyn std::error::Error>> {
// Initialize JWT for authentication // Initialize JWT for authentication
@@ -43,6 +45,7 @@ pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box<dyn std::error:
// Initialize services, passing the indexer sender to the relevant ones // Initialize services, passing the indexer sender to the relevant ones
let table_definition_service = TableDefinitionService { db_pool: db_pool.clone() }; let table_definition_service = TableDefinitionService { db_pool: db_pool.clone() };
let table_validation_service = TableValidationSvc { db: db_pool.clone() };
let tables_data_service = TablesDataService { let tables_data_service = TablesDataService {
db_pool: db_pool.clone(), db_pool: db_pool.clone(),
indexer_tx: indexer_tx.clone(), indexer_tx: indexer_tx.clone(),
@@ -55,6 +58,7 @@ pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box<dyn std::error:
Server::builder() Server::builder()
.add_service(TableStructureServiceServer::new(TableStructureHandler { db_pool: db_pool.clone() })) .add_service(TableStructureServiceServer::new(TableStructureHandler { db_pool: db_pool.clone() }))
.add_service(TableDefinitionServer::new(table_definition_service)) .add_service(TableDefinitionServer::new(table_definition_service))
.add_service(TableValidationServiceServer::new(table_validation_service))
.add_service(TablesDataServer::new(tables_data_service)) .add_service(TablesDataServer::new(tables_data_service))
.add_service(TableScriptServer::new(table_script_service)) .add_service(TableScriptServer::new(table_script_service))
.add_service(AuthServiceServer::new(auth_service)) .add_service(AuthServiceServer::new(auth_service))

View File

@@ -2,6 +2,7 @@
pub mod table_structure_service; pub mod table_structure_service;
pub mod table_definition_service; pub mod table_definition_service;
pub mod table_validation_service;
pub mod tables_data_service; pub mod tables_data_service;
pub mod table_script_service; pub mod table_script_service;
pub mod auth_service; pub mod auth_service;
@@ -9,6 +10,7 @@ pub mod search2_service;
pub use table_structure_service::TableStructureHandler; pub use table_structure_service::TableStructureHandler;
pub use table_definition_service::TableDefinitionService; pub use table_definition_service::TableDefinitionService;
pub use table_validation_service::*;
pub use tables_data_service::TablesDataService; pub use tables_data_service::TablesDataService;
pub use table_script_service::TableScriptService; pub use table_script_service::TableScriptService;
pub use auth_service::AuthServiceImpl; pub use auth_service::AuthServiceImpl;

View File

@@ -0,0 +1,10 @@
// src/server/services/table_validation_service.rs
use sqlx::PgPool;
use common::proto::komp_ac::table_validation::table_validation_service_server::TableValidationServiceServer;
use crate::table_validation::get::service::TableValidationSvc;
pub fn svc(db: PgPool) -> TableValidationServiceServer<TableValidationSvc> {
TableValidationServiceServer::new(TableValidationSvc { db })
}

View File

@@ -3,21 +3,20 @@
use steel::steel_vm::engine::Engine; use steel::steel_vm::engine::Engine;
use steel::steel_vm::register_fn::RegisterFn; use steel::steel_vm::register_fn::RegisterFn;
use steel::rvals::SteelVal; use steel::rvals::SteelVal;
use super::functions::{SteelContext, convert_row_data_for_steel}; use super::functions::SteelContext;
use steel_decimal::registry::FunctionRegistry; use steel_decimal::registry::FunctionRegistry;
use sqlx::PgPool; use sqlx::PgPool;
use std::sync::Arc; use std::sync::Arc;
use std::collections::HashMap; use std::collections::HashMap;
use thiserror::Error; use thiserror::Error;
use tracing::{debug, error}; use tracing::{debug, error};
use regex::Regex; // NEW
/// Represents different types of values that can be returned from Steel script execution.
#[derive(Debug)] #[derive(Debug)]
pub enum Value { pub enum Value {
Strings(Vec<String>), Strings(Vec<String>),
} }
/// Errors that can occur during Steel script execution.
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum ExecutionError { pub enum ExecutionError {
#[error("Script execution failed: {0}")] #[error("Script execution failed: {0}")]
@@ -28,7 +27,83 @@ pub enum ExecutionError {
UnsupportedType(String), UnsupportedType(String),
} }
/// Creates a Steel execution context with proper boolean value conversion. // NEW: upgrade steel_get_column -> steel_get_column_with_index using FK present in row_data
fn auto_promote_with_index(
script: &str,
current_table: &str,
row_data: &HashMap<String, String>,
) -> String {
// Matches: (steel_get_column "table" "column")
let re = Regex::new(
r#"\(\s*steel_get_column\s+"([^"]+)"\s+"([^"]+)"\s*\)"#,
)
.unwrap();
re.replace_all(script, |caps: &regex::Captures| {
let table = caps.get(1).unwrap().as_str();
let column = caps.get(2).unwrap().as_str();
// Only upgrade cross-table calls, if FK is present in the request data
if table != current_table {
let fk_key = format!("{}_id", table);
if let Some(id_str) = row_data.get(&fk_key) {
if let Ok(_) = id_str.parse::<i64>() {
return format!(
r#"(steel_get_column_with_index "{}" {} "{}")"#,
table, id_str, column
);
}
}
}
// Default: keep original call
caps.get(0).unwrap().as_str().to_string()
})
.into_owned()
}
use common::proto::komp_ac::table_definition::ColumnDefinition;
// Converts row data boolean values to Steel script format during context initialization.
pub async fn convert_row_data_for_steel(
db_pool: &PgPool,
schema_id: i64,
table_name: &str,
row_data: &mut HashMap<String, String>,
) -> Result<(), sqlx::Error> {
let table_def = sqlx::query!(
r#"
SELECT columns FROM table_definitions
WHERE schema_id = $1 AND table_name = $2
"#,
schema_id,
table_name
)
.fetch_optional(db_pool)
.await?
.ok_or_else(|| sqlx::Error::RowNotFound)?;
// Parse column definitions to identify boolean columns
if let Ok(columns) = serde_json::from_value::<Vec<ColumnDefinition>>(table_def.columns) {
for col_def in columns {
let normalized_type =
col_def.field_type.to_uppercase().split('(').next().unwrap().to_string();
if normalized_type == "BOOLEAN" || normalized_type == "BOOL" {
if let Some(value) = row_data.get_mut(&col_def.name) {
*value = match value.to_lowercase().as_str() {
"true" | "t" | "1" | "yes" | "on" => "#true".to_string(),
"false" | "f" | "0" | "no" | "off" => "#false".to_string(),
_ => value.clone(),
};
}
}
}
}
Ok(())
}
pub async fn create_steel_context_with_boolean_conversion( pub async fn create_steel_context_with_boolean_conversion(
current_table: String, current_table: String,
schema_id: i64, schema_id: i64,
@@ -36,7 +111,6 @@ pub async fn create_steel_context_with_boolean_conversion(
mut row_data: HashMap<String, String>, mut row_data: HashMap<String, String>,
db_pool: Arc<PgPool>, db_pool: Arc<PgPool>,
) -> Result<SteelContext, ExecutionError> { ) -> Result<SteelContext, ExecutionError> {
// Convert boolean values in row_data to Steel format
convert_row_data_for_steel(&db_pool, schema_id, &current_table, &mut row_data) convert_row_data_for_steel(&db_pool, schema_id, &current_table, &mut row_data)
.await .await
.map_err(|e| { .map_err(|e| {
@@ -53,7 +127,6 @@ pub async fn create_steel_context_with_boolean_conversion(
}) })
} }
/// Executes a Steel script with database context and type-safe result processing.
pub async fn execute_script( pub async fn execute_script(
script: String, script: String,
target_type: &str, target_type: &str,
@@ -65,42 +138,40 @@ pub async fn execute_script(
) -> Result<Value, ExecutionError> { ) -> Result<Value, ExecutionError> {
let mut vm = Engine::new(); let mut vm = Engine::new();
// Create execution context with proper boolean value conversion // Upgrade to with_index based on FK presence in the posted data
let script = auto_promote_with_index(&script, &current_table, &row_data);
let context = create_steel_context_with_boolean_conversion( let context = create_steel_context_with_boolean_conversion(
current_table, current_table.clone(),
schema_id, schema_id,
schema_name, schema_name,
row_data, row_data.clone(),
db_pool.clone(), db_pool.clone(),
).await?; )
.await?;
let context = Arc::new(context); let context = Arc::new(context);
// Register database access functions
register_steel_functions(&mut vm, context.clone()); register_steel_functions(&mut vm, context.clone());
// Register decimal math operations
register_decimal_math_functions(&mut vm); register_decimal_math_functions(&mut vm);
// Register row data as variables in the Steel VM for get-var access
let mut define_script = String::new(); let mut define_script = String::new();
for (key, value) in &context.row_data { for (key, value) in &context.row_data {
// Register only bare variable names for get-var access
define_script.push_str(&format!("(define {} \"{}\")\n", key, value)); define_script.push_str(&format!("(define {} \"{}\")\n", key, value));
} }
// Execute variable definitions if any exist
if !define_script.is_empty() { if !define_script.is_empty() {
vm.compile_and_run_raw_program(define_script) vm.compile_and_run_raw_program(define_script)
.map_err(|e| ExecutionError::RuntimeError(format!("Failed to register variables: {}", e)))?; .map_err(|e| ExecutionError::RuntimeError(format!(
"Failed to register variables: {}",
e
)))?;
} }
// Also register variables using the decimal registry as backup method
FunctionRegistry::register_variables(&mut vm, context.row_data.clone()); FunctionRegistry::register_variables(&mut vm, context.row_data.clone());
// Execute the main script let results = vm
let results = vm.compile_and_run_raw_program(script.clone()) .compile_and_run_raw_program(script.clone())
.map_err(|e| { .map_err(|e| {
error!("Steel script execution failed: {}", e); error!("Steel script execution failed: {}", e);
error!("Script was: {}", script); error!("Script was: {}", script);
@@ -108,22 +179,22 @@ pub async fn execute_script(
ExecutionError::RuntimeError(e.to_string()) ExecutionError::RuntimeError(e.to_string())
})?; })?;
// Convert results to the requested target type
match target_type { match target_type {
"STRINGS" => process_string_results(results), "STRINGS" => process_string_results(results),
_ => Err(ExecutionError::UnsupportedType(target_type.into())) _ => Err(ExecutionError::UnsupportedType(target_type.into())),
} }
} }
/// Registers Steel functions for database access within the VM context.
fn register_steel_functions(vm: &mut Engine, context: Arc<SteelContext>) { fn register_steel_functions(vm: &mut Engine, context: Arc<SteelContext>) {
debug!("Registering Steel functions with context"); debug!("Registering Steel functions with context");
// Register column access function for current and related tables
vm.register_fn("steel_get_column", { vm.register_fn("steel_get_column", {
let ctx = context.clone(); let ctx = context.clone();
move |table: String, column: String| { move |table: String, column: String| {
debug!("steel_get_column called with table: '{}', column: '{}'", table, column); debug!(
"steel_get_column called with table: '{}', column: '{}'",
table, column
);
ctx.steel_get_column(&table, &column) ctx.steel_get_column(&table, &column)
.map_err(|e| { .map_err(|e| {
error!("steel_get_column failed: {:?}", e); error!("steel_get_column failed: {:?}", e);
@@ -132,11 +203,13 @@ fn register_steel_functions(vm: &mut Engine, context: Arc<SteelContext>) {
} }
}); });
// Register indexed column access for comma-separated values
vm.register_fn("steel_get_column_with_index", { vm.register_fn("steel_get_column_with_index", {
let ctx = context.clone(); let ctx = context.clone();
move |table: String, index: i64, column: String| { move |table: String, index: i64, column: String| {
debug!("steel_get_column_with_index called with table: '{}', index: {}, column: '{}'", table, index, column); debug!(
"steel_get_column_with_index called with table: '{}', index: {}, column: '{}'",
table, index, column
);
ctx.steel_get_column_with_index(&table, index, &column) ctx.steel_get_column_with_index(&table, index, &column)
.map_err(|e| { .map_err(|e| {
error!("steel_get_column_with_index failed: {:?}", e); error!("steel_get_column_with_index failed: {:?}", e);
@@ -145,13 +218,11 @@ fn register_steel_functions(vm: &mut Engine, context: Arc<SteelContext>) {
} }
}); });
// Register safe SQL query execution
vm.register_fn("steel_query_sql", { vm.register_fn("steel_query_sql", {
let ctx = context.clone(); let ctx = context.clone();
move |query: String| { move |query: String| {
debug!("steel_query_sql called with query: '{}'", query); debug!("steel_query_sql called with query: '{}'", query);
ctx.steel_query_sql(&query) ctx.steel_query_sql(&query).map_err(|e| {
.map_err(|e| {
error!("steel_query_sql failed: {:?}", e); error!("steel_query_sql failed: {:?}", e);
e.to_string() e.to_string()
}) })
@@ -159,13 +230,11 @@ fn register_steel_functions(vm: &mut Engine, context: Arc<SteelContext>) {
}); });
} }
/// Registers decimal mathematics functions in the Steel VM.
fn register_decimal_math_functions(vm: &mut Engine) { fn register_decimal_math_functions(vm: &mut Engine) {
debug!("Registering decimal math functions"); debug!("Registering decimal math functions");
FunctionRegistry::register_all(vm); FunctionRegistry::register_all(vm);
} }
/// Processes Steel script results into string format for consistent output.
fn process_string_results(results: Vec<SteelVal>) -> Result<Value, ExecutionError> { fn process_string_results(results: Vec<SteelVal>) -> Result<Value, ExecutionError> {
let mut strings = Vec::new(); let mut strings = Vec::new();
@@ -178,7 +247,7 @@ fn process_string_results(results: Vec<SteelVal>) -> Result<Value, ExecutionErro
_ => { _ => {
error!("Unexpected result type: {:?}", result); error!("Unexpected result type: {:?}", result);
return Err(ExecutionError::TypeConversionError( return Err(ExecutionError::TypeConversionError(
format!("Expected string-convertible type, got {:?}", result) format!("Expected string-convertible type, got {:?}", result),
)); ));
} }
}; };

View File

@@ -1,5 +1,6 @@
// src/steel/server/functions.rs // src/steel/server/functions.rs
use common::proto::komp_ac::table_definition::ColumnDefinition;
use steel::rvals::SteelVal; use steel::rvals::SteelVal;
use sqlx::PgPool; use sqlx::PgPool;
use std::collections::HashMap; use std::collections::HashMap;
@@ -21,10 +22,8 @@ pub enum FunctionError {
ProhibitedTypeAccess(String), ProhibitedTypeAccess(String),
} }
/// Data types that Steel scripts are prohibited from accessing for security reasons
const PROHIBITED_TYPES: &[&str] = &["BIGINT", "DATE", "TIMESTAMPTZ"]; const PROHIBITED_TYPES: &[&str] = &["BIGINT", "DATE", "TIMESTAMPTZ"];
/// Execution context for Steel scripts with database access capabilities.
#[derive(Clone)] #[derive(Clone)]
pub struct SteelContext { pub struct SteelContext {
pub current_table: String, pub current_table: String,
@@ -35,26 +34,11 @@ pub struct SteelContext {
} }
impl SteelContext { impl SteelContext {
/// Resolves a base table name to its full qualified name in the current schema. async fn get_column_type(
/// Used for foreign key relationship traversal in Steel scripts. &self,
pub async fn get_related_table_name(&self, base_name: &str) -> Result<String, FunctionError> { table_name: &str,
let table_def = sqlx::query!( column_name: &str,
r#"SELECT table_name FROM table_definitions ) -> Result<String, FunctionError> {
WHERE schema_id = $1 AND table_name LIKE $2"#,
self.schema_id,
format!("%_{}", base_name)
)
.fetch_optional(&*self.db_pool)
.await
.map_err(|e| FunctionError::DatabaseError(e.to_string()))?
.ok_or_else(|| FunctionError::TableNotFound(base_name.to_string()))?;
Ok(table_def.table_name)
}
/// Retrieves the SQL data type for a specific column in a table.
/// Parses the JSON column definitions to find type information.
async fn get_column_type(&self, table_name: &str, column_name: &str) -> Result<String, FunctionError> {
let table_def = sqlx::query!( let table_def = sqlx::query!(
r#"SELECT columns FROM table_definitions r#"SELECT columns FROM table_definitions
WHERE schema_id = $1 AND table_name = $2"#, WHERE schema_id = $1 AND table_name = $2"#,
@@ -66,49 +50,43 @@ impl SteelContext {
.map_err(|e| FunctionError::DatabaseError(e.to_string()))? .map_err(|e| FunctionError::DatabaseError(e.to_string()))?
.ok_or_else(|| FunctionError::TableNotFound(table_name.to_string()))?; .ok_or_else(|| FunctionError::TableNotFound(table_name.to_string()))?;
let columns: Vec<String> = serde_json::from_value(table_def.columns) let columns: Vec<ColumnDefinition> = serde_json::from_value(table_def.columns)
.map_err(|e| FunctionError::DatabaseError(format!("Invalid column data: {}", e)))?; .map_err(|e| FunctionError::DatabaseError(format!(
"Invalid column data: {}",
e
)))?;
// Parse column definitions to find the requested column type for col_def in columns {
for column_def in columns { if col_def.name == column_name {
let mut parts = column_def.split_whitespace(); return Ok(col_def.field_type.to_uppercase());
if let (Some(name), Some(data_type)) = (parts.next(), parts.next()) {
let column_name_clean = name.trim_matches('"');
if column_name_clean == column_name {
return Ok(data_type.to_string());
}
} }
} }
Err(FunctionError::ColumnNotFound(format!( Err(FunctionError::ColumnNotFound(format!(
"Column '{}' not found in table '{}'", "Column '{}' not found in table '{}'",
column_name, column_name, table_name
table_name
))) )))
} }
/// Converts database values to Steel script format based on column type.
/// Currently handles boolean conversion to Steel's #true/#false syntax.
fn convert_value_to_steel_format(&self, value: &str, column_type: &str) -> String { fn convert_value_to_steel_format(&self, value: &str, column_type: &str) -> String {
let normalized_type = normalize_data_type(column_type); let normalized_type = normalize_data_type(column_type);
match normalized_type.as_str() { match normalized_type.as_str() {
"BOOLEAN" | "BOOL" => { "BOOLEAN" | "BOOL" => match value.to_lowercase().as_str() {
// Convert database boolean representations to Steel boolean syntax
match value.to_lowercase().as_str() {
"true" | "t" | "1" | "yes" | "on" => "#true".to_string(), "true" | "t" | "1" | "yes" | "on" => "#true".to_string(),
"false" | "f" | "0" | "no" | "off" => "#false".to_string(), "false" | "f" | "0" | "no" | "off" => "#false".to_string(),
_ => value.to_string(), // Return as-is if not a recognized boolean _ => value.to_string(),
} },
}
"INTEGER" => value.to_string(), "INTEGER" => value.to_string(),
_ => value.to_string(), // Return as-is for other types _ => value.to_string(),
} }
} }
/// Validates that a column type is allowed for Steel script access. async fn validate_column_type_and_get_type(
/// Returns the column type if validation passes, error if prohibited. &self,
async fn validate_column_type_and_get_type(&self, table_name: &str, column_name: &str) -> Result<String, FunctionError> { table_name: &str,
column_name: &str,
) -> Result<String, FunctionError> {
let column_type = self.get_column_type(table_name, column_name).await?; let column_type = self.get_column_type(table_name, column_name).await?;
if is_prohibited_type(&column_type) { if is_prohibited_type(&column_type) {
@@ -124,15 +102,13 @@ impl SteelContext {
Ok(column_type) Ok(column_type)
} }
/// Retrieves column value from current table or related tables via foreign keys. pub fn steel_get_column(
/// &self,
/// # Behavior table: &str,
/// - Current table: Returns value directly from row_data with type conversion column: &str,
/// - Related table: Follows foreign key relationship and queries database ) -> Result<SteelVal, SteelVal> {
/// - All accesses are subject to prohibited type validation
pub fn steel_get_column(&self, table: &str, column: &str) -> Result<SteelVal, SteelVal> {
if table == self.current_table { if table == self.current_table {
// Access current table data with type validation // current table
let column_type = tokio::task::block_in_place(|| { let column_type = tokio::task::block_in_place(|| {
let handle = tokio::runtime::Handle::current(); let handle = tokio::runtime::Handle::current();
handle.block_on(async { handle.block_on(async {
@@ -145,70 +121,112 @@ impl SteelContext {
Err(e) => return Err(SteelVal::StringV(e.to_string().into())), Err(e) => return Err(SteelVal::StringV(e.to_string().into())),
}; };
return self.row_data.get(column) return self
.row_data
.get(column)
.map(|v| { .map(|v| {
let converted_value = self.convert_value_to_steel_format(v, &column_type); let converted =
SteelVal::StringV(converted_value.into()) self.convert_value_to_steel_format(v, &column_type);
SteelVal::StringV(converted.into())
}) })
.ok_or_else(|| SteelVal::StringV(format!("Column {} not found", column).into())); .ok_or_else(|| {
SteelVal::StringV(
format!("Column {} not found", column).into(),
)
});
} }
// Access related table via foreign key relationship // Cross-table via FK: use exact table name FK convention: "<table>_id"
let base_name = table.split_once('_')
.map(|(_, rest)| rest)
.unwrap_or(table);
let fk_column = format!("{}_id", base_name);
let fk_value = self.row_data.get(&fk_column)
.ok_or_else(|| SteelVal::StringV(format!("Foreign key {} not found", fk_column).into()))?;
let result = tokio::task::block_in_place(|| { let result = tokio::task::block_in_place(|| {
let handle = tokio::runtime::Handle::current(); let handle = tokio::runtime::Handle::current();
handle.block_on(async { handle.block_on(async {
let actual_table = self.get_related_table_name(base_name).await let fk_column = format!("{}_id", table);
.map_err(|e| SteelVal::StringV(e.to_string().into()))?; let fk_value = self
.row_data
.get(&fk_column)
.ok_or_else(|| {
FunctionError::ForeignKeyNotFound(format!(
"Foreign key column '{}' not found on '{}'",
fk_column, self.current_table
))
})?;
// Validate column type and get type information let column_type =
let column_type = self.validate_column_type_and_get_type(&actual_table, column).await self.validate_column_type_and_get_type(table, column)
.map_err(|e| SteelVal::StringV(e.to_string().into()))?; .await?;
// Query the related table for the column value let raw_value = sqlx::query_scalar::<_, String>(&format!(
let raw_value = sqlx::query_scalar::<_, String>( "SELECT \"{}\" FROM \"{}\".\"{}\" WHERE id = $1",
&format!("SELECT {} FROM \"{}\".\"{}\" WHERE id = $1", column, self.schema_name, actual_table) column, self.schema_name, table
))
.bind(
fk_value
.parse::<i64>()
.map_err(|_| {
FunctionError::DatabaseError(
"Invalid foreign key format".into(),
)
})?,
) )
.bind(fk_value.parse::<i64>().map_err(|_|
SteelVal::StringV("Invalid foreign key format".into()))?)
.fetch_one(&*self.db_pool) .fetch_one(&*self.db_pool)
.await .await
.map_err(|e| SteelVal::StringV(e.to_string().into()))?; .map_err(|e| FunctionError::DatabaseError(e.to_string()))?;
// Convert to appropriate Steel format let converted =
let converted_value = self.convert_value_to_steel_format(&raw_value, &column_type); self.convert_value_to_steel_format(&raw_value, &column_type);
Ok(converted_value) Ok::<String, FunctionError>(converted)
}) })
}); });
result.map(|v| SteelVal::StringV(v.into())) match result {
Ok(v) => Ok(SteelVal::StringV(v.into())),
Err(e) => Err(SteelVal::StringV(e.to_string().into())),
}
} }
/// Retrieves a specific indexed element from a comma-separated column value.
/// Useful for accessing elements from array-like string representations.
pub fn steel_get_column_with_index( pub fn steel_get_column_with_index(
&self, &self,
table: &str, table: &str,
index: i64, index: i64,
column: &str column: &str,
) -> Result<SteelVal, SteelVal> { ) -> Result<SteelVal, SteelVal> {
// Get the full value with proper type conversion // Cross-table: interpret 'index' as the row id to fetch directly
let value = self.steel_get_column(table, column)?; if table != self.current_table {
let result = tokio::task::block_in_place(|| {
let handle = tokio::runtime::Handle::current();
handle.block_on(async {
let column_type =
self.validate_column_type_and_get_type(table, column)
.await?;
let raw_value = sqlx::query_scalar::<_, String>(&format!(
"SELECT \"{}\" FROM \"{}\".\"{}\" WHERE id = $1",
column, self.schema_name, table
))
.bind(index)
.fetch_one(&*self.db_pool)
.await
.map_err(|e| FunctionError::DatabaseError(e.to_string()))?;
let converted = self
.convert_value_to_steel_format(&raw_value, &column_type);
Ok::<String, FunctionError>(converted)
})
});
return match result {
Ok(v) => Ok(SteelVal::StringV(v.into())),
Err(e) => Err(SteelVal::StringV(e.to_string().into())),
};
}
// Current table: existing behavior (index in comma-separated string)
let value = self.steel_get_column(table, column)?;
if let SteelVal::StringV(s) = value { if let SteelVal::StringV(s) = value {
let parts: Vec<_> = s.split(',').collect(); let parts: Vec<_> = s.split(',').collect();
if let Some(part) = parts.get(index as usize) { if let Some(part) = parts.get(index as usize) {
let trimmed_part = part.trim(); let trimmed = part.trim();
// Apply type conversion to the indexed part based on original column type
let column_type = tokio::task::block_in_place(|| { let column_type = tokio::task::block_in_place(|| {
let handle = tokio::runtime::Handle::current(); let handle = tokio::runtime::Handle::current();
handle.block_on(async { handle.block_on(async {
@@ -218,40 +236,35 @@ impl SteelContext {
match column_type { match column_type {
Ok(ct) => { Ok(ct) => {
let converted_part = self.convert_value_to_steel_format(trimmed_part, &ct); let converted =
Ok(SteelVal::StringV(converted_part.into())) self.convert_value_to_steel_format(trimmed, &ct);
} Ok(SteelVal::StringV(converted.into()))
Err(_) => {
// If type cannot be determined, return value as-is
Ok(SteelVal::StringV(trimmed_part.into()))
} }
Err(_) => Ok(SteelVal::StringV(trimmed.into())),
} }
} else { } else {
Err(SteelVal::StringV("Index out of bounds".into())) Err(SteelVal::StringV("Index out of bounds".into()))
} }
} else { } else {
Err(SteelVal::StringV("Expected comma-separated string".into())) Err(SteelVal::StringV(
"Expected comma-separated string".into(),
))
} }
} }
/// Executes read-only SQL queries from Steel scripts with safety restrictions.
///
/// # Security Features
/// - Only SELECT, SHOW, and EXPLAIN queries allowed
/// - Prohibited column type access validation
/// - Returns first column of all rows as comma-separated string
pub fn steel_query_sql(&self, query: &str) -> Result<SteelVal, SteelVal> { pub fn steel_query_sql(&self, query: &str) -> Result<SteelVal, SteelVal> {
if !is_read_only_query(query) { if !is_read_only_query(query) {
return Err(SteelVal::StringV( return Err(SteelVal::StringV("Only SELECT queries are allowed".into()));
"Only SELECT queries are allowed".into()
));
} }
if contains_prohibited_column_access(query) { if contains_prohibited_column_access(query) {
return Err(SteelVal::StringV(format!( return Err(SteelVal::StringV(
format!(
"SQL query may access prohibited column types. Steel scripts cannot access columns of type: {}", "SQL query may access prohibited column types. Steel scripts cannot access columns of type: {}",
PROHIBITED_TYPES.join(", ") PROHIBITED_TYPES.join(", ")
).into())); )
.into(),
));
} }
let pool = self.db_pool.clone(); let pool = self.db_pool.clone();
@@ -266,7 +279,8 @@ impl SteelContext {
let mut results = Vec::new(); let mut results = Vec::new();
for row in rows { for row in rows {
let val: String = row.try_get(0) let val: String = row
.try_get(0)
.map_err(|e| SteelVal::StringV(e.to_string().into()))?; .map_err(|e| SteelVal::StringV(e.to_string().into()))?;
results.push(val); results.push(val);
} }
@@ -279,85 +293,30 @@ impl SteelContext {
} }
} }
/// Checks if a data type is prohibited for Steel script access.
fn is_prohibited_type(data_type: &str) -> bool { fn is_prohibited_type(data_type: &str) -> bool {
let normalized_type = normalize_data_type(data_type); let normalized_type = normalize_data_type(data_type);
PROHIBITED_TYPES.iter().any(|&prohibited| normalized_type.starts_with(prohibited)) PROHIBITED_TYPES
.iter()
.any(|&prohibited| normalized_type.starts_with(prohibited))
} }
/// Normalizes data type strings for consistent comparison.
/// Handles variations like NUMERIC(10,2) by extracting base type.
fn normalize_data_type(data_type: &str) -> String { fn normalize_data_type(data_type: &str) -> String {
data_type.to_uppercase() data_type
.split('(') // Remove precision/scale from NUMERIC(x,y) .to_uppercase()
.split('(')
.next() .next()
.unwrap_or(data_type) .unwrap_or(data_type)
.trim() .trim()
.to_string() .to_string()
} }
/// Performs basic heuristic check for prohibited column type access in SQL queries.
/// Looks for common patterns that might indicate access to restricted types.
fn contains_prohibited_column_access(query: &str) -> bool { fn contains_prohibited_column_access(query: &str) -> bool {
let query_upper = query.to_uppercase(); let query_upper = query.to_uppercase();
let patterns = ["EXTRACT(", "DATE_PART(", "::DATE", "::TIMESTAMPTZ", "::BIGINT"];
let patterns = [ patterns.iter().any(|p| query_upper.contains(p))
"EXTRACT(", // Common with DATE/TIMESTAMPTZ
"DATE_PART(", // Common with DATE/TIMESTAMPTZ
"::DATE",
"::TIMESTAMPTZ",
"::BIGINT",
];
patterns.iter().any(|pattern| query_upper.contains(pattern))
} }
/// Validates that a query is read-only and safe for Steel script execution.
fn is_read_only_query(query: &str) -> bool { fn is_read_only_query(query: &str) -> bool {
let query = query.trim_start().to_uppercase(); let query = query.trim_start().to_uppercase();
query.starts_with("SELECT") || query.starts_with("SELECT") || query.starts_with("SHOW") || query.starts_with("EXPLAIN")
query.starts_with("SHOW") ||
query.starts_with("EXPLAIN")
}
/// Converts row data boolean values to Steel script format during context initialization.
pub async fn convert_row_data_for_steel(
db_pool: &PgPool,
schema_id: i64,
table_name: &str,
row_data: &mut HashMap<String, String>,
) -> Result<(), sqlx::Error> {
let table_def = sqlx::query!(
r#"SELECT columns FROM table_definitions
WHERE schema_id = $1 AND table_name = $2"#,
schema_id,
table_name
)
.fetch_optional(db_pool)
.await?
.ok_or_else(|| sqlx::Error::RowNotFound)?;
// Parse column definitions to identify boolean columns for conversion
if let Ok(columns) = serde_json::from_value::<Vec<String>>(table_def.columns) {
for column_def in columns {
let mut parts = column_def.split_whitespace();
if let (Some(name), Some(data_type)) = (parts.next(), parts.next()) {
let column_name = name.trim_matches('"');
let normalized_type = normalize_data_type(data_type);
if normalized_type == "BOOLEAN" || normalized_type == "BOOL" {
if let Some(value) = row_data.get_mut(column_name) {
// Convert boolean value to Steel format
*value = match value.to_lowercase().as_str() {
"true" | "t" | "1" | "yes" | "on" => "#true".to_string(),
"false" | "f" | "0" | "no" | "off" => "#false".to_string(),
_ => value.clone(), // Keep original if not recognized
};
}
}
}
}
}
Ok(())
} }

View File

@@ -2,24 +2,9 @@
use tonic::Status; use tonic::Status;
use sqlx::{PgPool, Transaction, Postgres}; use sqlx::{PgPool, Transaction, Postgres};
use serde_json::json;
use common::proto::komp_ac::table_definition::{PostTableDefinitionRequest, TableDefinitionResponse}; use common::proto::komp_ac::table_definition::{PostTableDefinitionRequest, TableDefinitionResponse};
use common::proto::komp_ac::table_definition::ColumnDefinition;
// TODO CRITICAL add decimal with optional precision" use crate::table_definition::models::map_field_type;
const PREDEFINED_FIELD_TYPES: &[(&str, &str)] = &[
("text", "TEXT"),
("string", "TEXT"),
("boolean", "BOOLEAN"),
("timestamp", "TIMESTAMPTZ"),
("timestamptz", "TIMESTAMPTZ"),
("time", "TIMESTAMPTZ"),
("money", "NUMERIC(14, 4)"),
("integer", "INTEGER"),
("int", "INTEGER"),
("biginteger", "BIGINT"),
("bigint", "BIGINT"),
("date", "DATE"),
];
// NEW: Helper function to provide detailed error messages // NEW: Helper function to provide detailed error messages
fn validate_identifier_format(s: &str, identifier_type: &str) -> Result<(), Status> { fn validate_identifier_format(s: &str, identifier_type: &str) -> Result<(), Status> {
@@ -58,116 +43,6 @@ fn validate_identifier_format(s: &str, identifier_type: &str) -> Result<(), Stat
Ok(()) Ok(())
} }
fn validate_decimal_number_format(num_str: &str, param_name: &str) -> Result<(), Status> {
if num_str.is_empty() {
return Err(Status::invalid_argument(format!(
"{} cannot be empty",
param_name
)));
}
// Check for explicit signs
if num_str.starts_with('+') || num_str.starts_with('-') {
return Err(Status::invalid_argument(format!(
"{} cannot have explicit positive or negative signs",
param_name
)));
}
// Check for decimal points
if num_str.contains('.') {
return Err(Status::invalid_argument(format!(
"{} must be a whole number (no decimal points)",
param_name
)));
}
// Check for leading zeros (but allow "0" itself)
if num_str.len() > 1 && num_str.starts_with('0') {
let trimmed = num_str.trim_start_matches('0');
let suggestion = if trimmed.is_empty() { "0" } else { trimmed };
return Err(Status::invalid_argument(format!(
"{} cannot have leading zeros (use '{}' instead of '{}')",
param_name,
suggestion,
num_str
)));
}
// Check that all characters are digits
if !num_str.chars().all(|c| c.is_ascii_digit()) {
return Err(Status::invalid_argument(format!(
"{} contains invalid characters. Only digits 0-9 are allowed",
param_name
)));
}
Ok(())
}
fn map_field_type(field_type: &str) -> Result<String, Status> {
let lower_field_type = field_type.to_lowercase();
// Special handling for "decimal(precision, scale)"
if lower_field_type.starts_with("decimal(") && lower_field_type.ends_with(')') {
// Extract the part inside the parentheses, e.g., "10, 2"
let args = lower_field_type
.strip_prefix("decimal(")
.and_then(|s| s.strip_suffix(')'))
.unwrap_or(""); // Should always succeed due to the checks above
// Split into precision and scale parts
if let Some((p_str, s_str)) = args.split_once(',') {
let precision_str = p_str.trim();
let scale_str = s_str.trim();
// NEW: Validate format BEFORE parsing
validate_decimal_number_format(precision_str, "precision")?;
validate_decimal_number_format(scale_str, "scale")?;
// Parse precision, returning an error if it's not a valid number
let precision = precision_str.parse::<u32>().map_err(|_| {
Status::invalid_argument("Invalid precision in decimal type")
})?;
// Parse scale, returning an error if it's not a valid number
let scale = scale_str.parse::<u32>().map_err(|_| {
Status::invalid_argument("Invalid scale in decimal type")
})?;
// Add validation based on PostgreSQL rules
if precision < 1 {
return Err(Status::invalid_argument("Precision must be at least 1"));
}
if scale > precision {
return Err(Status::invalid_argument(
"Scale cannot be greater than precision",
));
}
// If everything is valid, build and return the NUMERIC type string
return Ok(format!("NUMERIC({}, {})", precision, scale));
} else {
// The format was wrong, e.g., "decimal(10)" or "decimal()"
return Err(Status::invalid_argument(
"Invalid decimal format. Expected: decimal(precision, scale)",
));
}
}
// If not a decimal, fall back to the predefined list
PREDEFINED_FIELD_TYPES
.iter()
.find(|(key, _)| *key == lower_field_type.as_str())
.map(|(_, sql_type)| sql_type.to_string()) // Convert to an owned String
.ok_or_else(|| {
Status::invalid_argument(format!(
"Invalid field type: {}",
field_type
))
})
}
fn is_invalid_table_name(table_name: &str) -> bool { fn is_invalid_table_name(table_name: &str) -> bool {
table_name.ends_with("_id") || table_name.ends_with("_id") ||
table_name == "id" || table_name == "id" ||
@@ -299,7 +174,9 @@ async fn execute_table_definition(
links.push((linked_id, link.required)); links.push((linked_id, link.required));
} }
let mut columns = Vec::new(); let mut stored_columns = Vec::new();
let mut sql_columns = Vec::new();
for col_def in request.columns.drain(..) { for col_def in request.columns.drain(..) {
let col_name = col_def.name.trim().to_string(); let col_name = col_def.name.trim().to_string();
validate_identifier_format(&col_name, "Column name")?; validate_identifier_format(&col_name, "Column name")?;
@@ -312,21 +189,33 @@ async fn execute_table_definition(
} }
let sql_type = map_field_type(&col_def.field_type)?; let sql_type = map_field_type(&col_def.field_type)?;
columns.push(format!("\"{}\" {}", col_name, sql_type)); sql_columns.push(format!("\"{}\" {}", col_name, sql_type));
// push the proto type (serde serializable)
stored_columns.push(ColumnDefinition {
name: col_name,
field_type: col_def.field_type,
});
} }
// Indexes
let mut stored_indexes = Vec::new();
let mut indexes = Vec::new(); let mut indexes = Vec::new();
for idx in request.indexes.drain(..) { for idx in request.indexes.drain(..) {
let idx_name = idx.trim().to_string(); let idx_name = idx.trim().to_string();
validate_identifier_format(&idx_name, "Index name")?; validate_identifier_format(&idx_name, "Index name")?;
if !columns.iter().any(|c| c.starts_with(&format!("\"{}\"", idx_name))) { if !sql_columns.iter().any(|c| c.starts_with(&format!("\"{}\"", idx_name))) {
return Err(Status::invalid_argument(format!("Index column '{}' not found", idx_name))); return Err(Status::invalid_argument(format!(
"Index column '{}' not found", idx_name
)));
} }
stored_indexes.push(idx_name.clone());
indexes.push(idx_name); indexes.push(idx_name);
} }
let (create_sql, index_sql) = generate_table_sql(tx, &profile_name, &table_name, &columns, &indexes, &links).await?; let (create_sql, index_sql) = generate_table_sql(tx, &profile_name, &table_name, &sql_columns, &indexes, &links).await?;
// Use schema_id instead of profile_id // Use schema_id instead of profile_id
let table_def = sqlx::query!( let table_def = sqlx::query!(
@@ -336,8 +225,8 @@ async fn execute_table_definition(
RETURNING id"#, RETURNING id"#,
schema.id, schema.id,
&table_name, &table_name,
json!(columns), serde_json::to_value(&stored_columns).unwrap(),
json!(indexes) serde_json::to_value(&stored_indexes).unwrap()
) )
.fetch_one(&mut **tx) .fetch_one(&mut **tx)
.await .await
@@ -351,6 +240,29 @@ async fn execute_table_definition(
Status::internal(format!("Database error: {}", e)) Status::internal(format!("Database error: {}", e))
})?; })?;
for col_def in &sql_columns {
// Column string looks like "\"name\" TYPE", split out identifier
let col_name = col_def.split_whitespace().next().unwrap_or("");
let clean_col = col_name.trim_matches('"');
// Default empty config — currently only character_limits block, none set.
let default_cfg = serde_json::json!({
"character_limits": { "min": 0, "max": 0, "warn_at": null, "count_mode": "CHARS" }
});
sqlx::query!(
r#"INSERT INTO table_validation_rules (table_def_id, data_key, config)
VALUES ($1, $2, $3)
ON CONFLICT (table_def_id, data_key) DO NOTHING"#,
table_def.id,
clean_col,
default_cfg
)
.execute(&mut **tx)
.await
.map_err(|e| Status::internal(format!("Failed to insert default validation rule for column {}: {}", clean_col, e)))?;
}
for (linked_id, is_required) in links { for (linked_id, is_required) in links {
sqlx::query!( sqlx::query!(
"INSERT INTO table_definition_links "INSERT INTO table_definition_links

View File

@@ -2,3 +2,6 @@
pub mod models; pub mod models;
pub mod handlers; pub mod handlers;
pub mod repo;
pub use repo::*;

View File

@@ -0,0 +1,91 @@
// src/table_definition/models.rs
use tonic::Status;
/// Predefined static field mappings
// TODO CRITICAL add decimal with optional precision"
pub const PREDEFINED_FIELD_TYPES: &[(&str, &str)] = &[
("text", "TEXT"),
("string", "TEXT"),
("boolean", "BOOLEAN"),
("timestamp", "TIMESTAMPTZ"),
("timestamptz", "TIMESTAMPTZ"),
("time", "TIMESTAMPTZ"),
("money", "NUMERIC(14, 4)"),
("integer", "INTEGER"),
("int", "INTEGER"),
("biginteger", "BIGINT"),
("bigint", "BIGINT"),
("date", "DATE"),
];
/// reusable decimal number validation
pub fn validate_decimal_number_format(num_str: &str, param_name: &str) -> Result<(), Status> {
if num_str.is_empty() {
return Err(Status::invalid_argument(format!("{} cannot be empty", param_name)));
}
if num_str.starts_with('+') || num_str.starts_with('-') {
return Err(Status::invalid_argument(format!(
"{} cannot have explicit positive/negative signs", param_name
)));
}
if num_str.contains('.') {
return Err(Status::invalid_argument(format!(
"{} must be a whole number (no decimal point)", param_name
)));
}
if num_str.len() > 1 && num_str.starts_with('0') {
let trimmed = num_str.trim_start_matches('0');
let suggestion = if trimmed.is_empty() { "0" } else { trimmed };
return Err(Status::invalid_argument(format!(
"{} cannot have leading zeros (use '{}' instead of '{}')",
param_name, suggestion, num_str
)));
}
if !num_str.chars().all(|c| c.is_ascii_digit()) {
return Err(Status::invalid_argument(format!(
"{} contains invalid characters. Only digits allowed", param_name
)));
}
Ok(())
}
/// reusable field type mapper
pub fn map_field_type(field_type: &str) -> Result<String, Status> {
let lower_field_type = field_type.to_lowercase();
if lower_field_type.starts_with("decimal(") && lower_field_type.ends_with(')') {
let args = lower_field_type.strip_prefix("decimal(").unwrap()
.strip_suffix(')').unwrap();
if let Some((p_str, s_str)) = args.split_once(',') {
let precision_str = p_str.trim();
let scale_str = s_str.trim();
validate_decimal_number_format(precision_str, "precision")?;
validate_decimal_number_format(scale_str, "scale")?;
let precision = precision_str.parse::<u32>()
.map_err(|_| Status::invalid_argument("Invalid precision"))?;
let scale = scale_str.parse::<u32>()
.map_err(|_| Status::invalid_argument("Invalid scale"))?;
if precision < 1 {
return Err(Status::invalid_argument("Precision must be >= 1"));
}
if scale > precision {
return Err(Status::invalid_argument("Scale cannot be > precision"));
}
return Ok(format!("NUMERIC({}, {})", precision, scale));
} else {
return Err(Status::invalid_argument(
"Invalid decimal format. Expected decimal(precision, scale)"
));
}
}
PREDEFINED_FIELD_TYPES
.iter()
.find(|(key, _)| *key == lower_field_type.as_str())
.map(|(_, sql_type)| sql_type.to_string())
.ok_or_else(|| Status::invalid_argument(format!("Invalid field type: {}", field_type)))
}

View File

@@ -0,0 +1,33 @@
// src/table_definition/repo.rs
use common::proto::komp_ac::table_definition::ColumnDefinition;
use sqlx::PgPool;
pub struct TableDefRow {
pub id: i64,
pub table_name: String,
pub columns: Vec<ColumnDefinition>,
pub indexes: Vec<String>,
}
pub async fn get_table_definition(
db: &PgPool,
id: i64,
) -> Result<TableDefRow, anyhow::Error> {
let rec = sqlx::query!(
r#"
SELECT id, table_name, columns, indexes
FROM table_definitions
WHERE id = $1
"#,
id
)
.fetch_one(db)
.await?;
Ok(TableDefRow {
id: rec.id,
table_name: rec.table_name,
columns: serde_json::from_value(rec.columns)?, // 🔑
indexes: serde_json::from_value(rec.indexes)?,
})
}

View File

@@ -2,7 +2,8 @@
use std::collections::HashMap; use std::collections::HashMap;
use tonic::Status; use tonic::Status;
use serde_json::{json, Value}; use serde::{Deserialize, Serialize};
use serde_json::Value;
/// Represents the state of a node during dependency graph traversal. /// Represents the state of a node during dependency graph traversal.
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq)]
@@ -40,18 +41,38 @@ impl DependencyType {
DependencyType::SqlQuery { .. } => "sql_query", DependencyType::SqlQuery { .. } => "sql_query",
} }
} }
}
/// Generates context JSON for database storage. /// Strongly-typed JSON for script_dependencies.context_info
pub fn context_json(&self) -> Value { /// Using untagged so JSON stays minimal (no "type" field), and we can still
/// deserialize it into a proper enum.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ScriptDependencyContext {
ColumnAccess { column: String },
IndexedAccess { column: String, index: i64 },
SqlQuery { query_fragment: String },
}
impl DependencyType {
/// Convert this dependency into its JSON context struct.
pub fn to_context(&self) -> ScriptDependencyContext {
match self { match self {
DependencyType::ColumnAccess { column } => { DependencyType::ColumnAccess { column } => {
json!({ "column": column }) ScriptDependencyContext::ColumnAccess {
column: column.clone(),
}
} }
DependencyType::IndexedAccess { column, index } => { DependencyType::IndexedAccess { column, index } => {
json!({ "column": column, "index": index }) ScriptDependencyContext::IndexedAccess {
column: column.clone(),
index: *index,
}
} }
DependencyType::SqlQuery { query_fragment } => { DependencyType::SqlQuery { query_fragment } => {
json!({ "query_fragment": query_fragment }) ScriptDependencyContext::SqlQuery {
query_fragment: query_fragment.clone(),
}
} }
} }
} }
@@ -554,7 +575,7 @@ impl DependencyAnalyzer {
table_id, table_id,
target_id, target_id,
dep.dependency_type.as_str(), dep.dependency_type.as_str(),
dep.dependency_type.context_json() serde_json::to_value(dep.dependency_type.to_context()).unwrap()
) )
.execute(&mut **tx) .execute(&mut **tx)
.await .await

View File

@@ -4,6 +4,7 @@
use tonic::Status; use tonic::Status;
use sqlx::{PgPool, Error as SqlxError}; use sqlx::{PgPool, Error as SqlxError};
use common::proto::komp_ac::table_script::{PostTableScriptRequest, TableScriptResponse}; use common::proto::komp_ac::table_script::{PostTableScriptRequest, TableScriptResponse};
use common::proto::komp_ac::table_definition::ColumnDefinition;
use serde_json::Value; use serde_json::Value;
use steel_decimal::SteelDecimal; use steel_decimal::SteelDecimal;
use regex::Regex; use regex::Regex;
@@ -303,16 +304,12 @@ async fn validate_math_operations_column_types(
let mut table_column_types: HashMap<String, HashMap<String, String>> = HashMap::new(); let mut table_column_types: HashMap<String, HashMap<String, String>> = HashMap::new();
for table_def in table_definitions { for table_def in table_definitions {
let columns: Vec<String> = serde_json::from_value(table_def.columns) let columns: Vec<ColumnDefinition> = serde_json::from_value(table_def.columns)
.map_err(|e| Status::internal(format!("Invalid column data for table '{}': {}", table_def.table_name, e)))?; .map_err(|e| Status::internal(format!("Invalid column data for table '{}': {}", table_def.table_name, e)))?;
let mut column_types = HashMap::new(); let mut column_types = HashMap::new();
for column_def in columns { for col_def in columns {
let mut parts = column_def.split_whitespace(); column_types.insert(col_def.name.clone(), col_def.field_type.clone());
if let (Some(name), Some(data_type)) = (parts.next(), parts.next()) {
let column_name = name.trim_matches('"');
column_types.insert(column_name.to_string(), data_type.to_string());
}
} }
table_column_types.insert(table_def.table_name, column_types); table_column_types.insert(table_def.table_name, column_types);
} }
@@ -363,25 +360,13 @@ fn validate_target_column(
} }
// Parse the columns JSON into a vector of strings // Parse the columns JSON into a vector of strings
let columns: Vec<String> = serde_json::from_value(table_columns.clone()) let columns: Vec<ColumnDefinition> = serde_json::from_value(table_columns.clone())
.map_err(|e| format!("Invalid column data: {}", e))?; .map_err(|e| format!("Invalid column data: {}", e))?;
// Extract column names and types let column_type = columns
let column_info: Vec<(&str, &str)> = columns
.iter() .iter()
.filter_map(|c| { .find(|c| c.name == target)
let mut parts = c.split_whitespace(); .map(|c| c.field_type.clone())
let name = parts.next()?.trim_matches('"');
let data_type = parts.next()?;
Some((name, data_type))
})
.collect();
// Find the target column and return its type
let column_type = column_info
.iter()
.find(|(name, _)| *name == target)
.map(|(_, dt)| dt.to_string())
.ok_or_else(|| format!("Target column '{}' not defined in table '{}'", target, table_name))?; .ok_or_else(|| format!("Target column '{}' not defined in table '{}'", target, table_name))?;
// Check if the target column type is prohibited // Check if the target column type is prohibited
@@ -509,34 +494,21 @@ async fn validate_script_column_references(
/// Validate that a referenced column doesn't have a prohibited type /// Validate that a referenced column doesn't have a prohibited type
fn validate_referenced_column_type(table_name: &str, column_name: &str, table_columns: &Value) -> Result<(), String> { fn validate_referenced_column_type(table_name: &str, column_name: &str, table_columns: &Value) -> Result<(), String> {
// Parse the columns JSON into a vector of strings // Parse the columns JSON into a vector of strings
let columns: Vec<String> = serde_json::from_value(table_columns.clone()) let columns: Vec<ColumnDefinition> = serde_json::from_value(table_columns.clone())
.map_err(|e| format!("Invalid column data for table '{}': {}", table_name, e))?; .map_err(|e| format!("Invalid column data for table '{}': {}", table_name, e))?;
// Extract column names and types if let Some(col_def) = columns.iter().find(|c| c.name == column_name) {
let column_info: Vec<(&str, &str)> = columns if is_prohibited_type(&col_def.field_type) {
.iter()
.filter_map(|c| {
let mut parts = c.split_whitespace();
let name = parts.next()?.trim_matches('"');
let data_type = parts.next()?;
Some((name, data_type))
})
.collect();
// Find the referenced column and check its type
if let Some((_, column_type)) = column_info.iter().find(|(name, _)| *name == column_name) {
if is_prohibited_type(column_type) {
return Err(format!( return Err(format!(
"Script references column '{}' in table '{}' which has prohibited type '{}'. Steel scripts cannot access columns of type: {}", "Script references column '{}' in table '{}' which has prohibited type '{}'. Steel scripts cannot access columns of type: {}",
column_name, column_name,
table_name, table_name,
column_type, col_def.field_type,
PROHIBITED_TYPES.join(", ") PROHIBITED_TYPES.join(", ")
)); ));
} }
// Log info for boolean columns let normalized_type = normalize_data_type(&col_def.field_type);
let normalized_type = normalize_data_type(column_type);
if normalized_type == "BOOLEAN" || normalized_type == "BOOL" { if normalized_type == "BOOLEAN" || normalized_type == "BOOL" {
println!("Info: Script references boolean column '{}' in table '{}'. Values will be converted to Steel format (#true/#false)", column_name, table_name); println!("Info: Script references boolean column '{}' in table '{}'. Values will be converted to Steel format (#true/#false)", column_name, table_name);
} }

View File

@@ -1,4 +1,7 @@
// src/table_script/mod.rs // src/table_script/mod.rs
pub mod handlers; pub mod handlers;
pub mod repo;
pub use handlers::*; pub use handlers::*;
pub use repo::*;

View File

@@ -0,0 +1,49 @@
// src/table_script/repo.rs
use anyhow::Result;
use sqlx::PgPool;
use crate::table_script::handlers::dependency_analyzer::ScriptDependencyContext;
#[derive(Debug, Clone)]
pub struct ScriptDependencyRecord {
pub script_id: i64,
pub source_table_id: i64,
pub target_table_id: i64,
pub dependency_type: String,
pub context: Option<ScriptDependencyContext>,
}
pub async fn get_dependencies_for_script(
db: &PgPool,
script_id: i64,
) -> Result<Vec<ScriptDependencyRecord>> {
let rows = sqlx::query!(
r#"
SELECT script_id, source_table_id, target_table_id, dependency_type, context_info
FROM script_dependencies
WHERE script_id = $1
ORDER BY source_table_id, target_table_id
"#,
script_id
)
.fetch_all(db)
.await?;
let mut out = Vec::new();
for r in rows {
let context = match r.context_info {
Some(value) => Some(serde_json::from_value::<ScriptDependencyContext>(value)?),
None => None,
};
out.push(ScriptDependencyRecord {
script_id: r.script_id,
source_table_id: r.source_table_id,
target_table_id: r.target_table_id,
dependency_type: r.dependency_type,
context,
});
}
Ok(out)
}

View File

@@ -0,0 +1,2 @@
// src/table_validation/get/mod.rs
pub mod service;

View File

@@ -0,0 +1,113 @@
// src/table_validation/get/service.rs
use tonic::{Request, Response, Status};
use sqlx::PgPool;
use common::proto::komp_ac::table_validation::{
table_validation_service_server::TableValidationService,
GetTableValidationRequest, TableValidationResponse,
UpdateFieldValidationRequest, UpdateFieldValidationResponse,
FieldValidation,
};
use crate::table_validation::post::repo; // repo still lives in post
pub struct TableValidationSvc {
pub db: PgPool,
}
#[tonic::async_trait]
impl TableValidationService for TableValidationSvc {
async fn get_table_validation(
&self,
req: Request<GetTableValidationRequest>,
) -> Result<Response<TableValidationResponse>, Status> {
let req = req.into_inner();
// 1. Get table_def_id
let table_def_id = repo::get_table_def_id(&self.db, &req.profile_name, &req.table_name)
.await
.map_err(|_| Status::not_found("Table definition not found"))?;
// 2. Get validations
let rules = repo::get_validations(&self.db, table_def_id)
.await
.map_err(|e| Status::internal(format!("Failed to fetch rules: {}", e)))?;
// 3. Parse JSON directly into proto types
let mut fields_out = Vec::new();
for r in rules {
match serde_json::from_value::<FieldValidation>(r.config) {
Ok(mut fv) => {
// Set the data_key from the database row
fv.data_key = r.data_key;
// Keep entries that have either meaningful limits or a mask
let has_meaningful_limits = fv
.limits
.as_ref()
.map_or(false, |l| l.min > 0 || l.max > 0 || l.warn_at.is_some());
let has_mask = fv.mask.is_some();
if !has_meaningful_limits && !has_mask {
continue;
}
fields_out.push(fv);
}
Err(e) => {
tracing::warn!("Invalid JSON for {}: {}", r.data_key, e);
continue;
}
}
}
Ok(Response::new(TableValidationResponse { fields: fields_out }))
}
async fn update_field_validation(
&self,
req: Request<UpdateFieldValidationRequest>,
) -> Result<Response<UpdateFieldValidationResponse>, Status> {
let req = req.into_inner();
let table_def_id = repo::get_table_def_id(
&self.db, &req.profile_name, &req.table_name,
)
.await
.map_err(|_| Status::not_found("Table definition not found"))?;
// Check if validation is provided
if let Some(validation) = req.validation {
// Convert proto FieldValidation directly to JSON
let json_value = serde_json::to_value(&validation)
.map_err(|e| Status::internal(format!("serialize error: {e}")))?;
sqlx::query!(
r#"
INSERT INTO table_validation_rules (
table_def_id, data_key, config, updated_at
)
VALUES ($1, $2, $3, now())
ON CONFLICT (table_def_id, data_key)
DO UPDATE SET
config = EXCLUDED.config,
updated_at = now()
"#,
table_def_id,
req.data_key,
json_value
)
.execute(&self.db)
.await
.map_err(|e| Status::internal(format!("DB error: {e}")))?;
Ok(Response::new(UpdateFieldValidationResponse {
success: true,
message: format!(
"Validation rules updated for {}.{} column {}",
req.profile_name, req.table_name, req.data_key
),
}))
} else {
Err(Status::invalid_argument("No validation provided"))
}
}
}

View File

@@ -0,0 +1,4 @@
// src/table_validation/mod.rs
pub mod post;
pub mod get;

View File

@@ -0,0 +1,3 @@
// src/table_validation/post/mod.rs
pub mod repo;

View File

@@ -0,0 +1,52 @@
// src/table_validation/repo.rs
use sqlx::PgPool;
use anyhow::Result;
use serde_json::Value;
pub struct ValidationRuleRow {
pub data_key: String,
pub config: Value,
}
pub async fn get_table_def_id(
db: &PgPool,
profile_name: &str,
table_name: &str,
) -> Result<i64> {
let rec = sqlx::query!(
r#"
SELECT td.id
FROM table_definitions td
JOIN schemas s ON td.schema_id = s.id
WHERE s.name = $1 AND td.table_name = $2
"#,
profile_name,
table_name
)
.fetch_one(db)
.await?;
Ok(rec.id)
}
pub async fn get_validations(
db: &PgPool,
table_def_id: i64,
) -> Result<Vec<ValidationRuleRow>> {
let rows = sqlx::query!(
r#"
SELECT data_key, config
FROM table_validation_rules
WHERE table_def_id = $1
"#,
table_def_id
)
.fetch_all(db)
.await?;
Ok(rows.into_iter().map(|r| ValidationRuleRow {
data_key: r.data_key,
config: r.config,
}).collect())
}

View File

@@ -0,0 +1,18 @@
// src/table_validation/post/service.rs
use tonic::{Request, Response, Status};
use sqlx::PgPool;
use common::proto::komp_ac::table_validation::{
UpdateFieldValidationRequest, UpdateFieldValidationResponse,
};
use crate::table_validation::post::repo;
pub struct TableValidationUpdateSvc {
pub db: PgPool,
}
impl TableValidationUpdateSvc {
pub fn new(db: PgPool) -> Self {
Self { db }
}
}

View File

@@ -1,9 +1,11 @@
// src/tables_data/handlers/get_table_data.rs // src/tables_data/handlers/get_table_data.rs
use tonic::Status; use tonic::Status;
use sqlx::{PgPool, Row}; use sqlx::{PgPool, Row};
use std::collections::HashMap; use std::collections::HashMap;
use common::proto::komp_ac::tables_data::{GetTableDataRequest, GetTableDataResponse}; use common::proto::komp_ac::tables_data::{GetTableDataRequest, GetTableDataResponse};
use common::proto::komp_ac::table_definition::ColumnDefinition;
use crate::shared::schema_qualifier::qualify_table_name_for_data; use crate::shared::schema_qualifier::qualify_table_name_for_data;
pub async fn get_table_data( pub async fn get_table_data(
@@ -39,17 +41,13 @@ pub async fn get_table_data(
let table_def = table_def.ok_or_else(|| Status::not_found("Table not found"))?; let table_def = table_def.ok_or_else(|| Status::not_found("Table not found"))?;
// Parse user-defined columns from JSON // Parse user-defined columns from JSON
let columns_json: Vec<String> = serde_json::from_value(table_def.columns.clone()) let stored_columns: Vec<ColumnDefinition> = serde_json::from_value(table_def.columns.clone())
.map_err(|e| Status::internal(format!("Column parsing error: {}", e)))?; .map_err(|e| Status::internal(format!("Column parsing error: {}", e)))?;
// Directly extract names, no split(" ") parsing anymore
let mut user_columns = Vec::new(); let mut user_columns = Vec::new();
for col_def in columns_json { for col_def in stored_columns {
let parts: Vec<&str> = col_def.splitn(2, ' ').collect(); user_columns.push(col_def.name.trim().to_string());
if parts.len() != 2 {
return Err(Status::internal("Invalid column format"));
}
let name = parts[0].trim_matches('"').to_string();
user_columns.push(name);
} }
// --- START OF FIX --- // --- START OF FIX ---

View File

@@ -5,6 +5,8 @@ use sqlx::{PgPool, Arguments};
use sqlx::postgres::PgArguments; use sqlx::postgres::PgArguments;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use common::proto::komp_ac::tables_data::{PostTableDataRequest, PostTableDataResponse}; use common::proto::komp_ac::tables_data::{PostTableDataRequest, PostTableDataResponse};
use common::proto::komp_ac::table_definition::ColumnDefinition;
use crate::table_definition::models::map_field_type;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use prost_types::value::Kind; use prost_types::value::Kind;
@@ -56,18 +58,16 @@ pub async fn post_table_data(
let table_def = table_def.ok_or_else(|| Status::not_found("Table not found"))?; let table_def = table_def.ok_or_else(|| Status::not_found("Table not found"))?;
// Parse column definitions from JSON format // Parse column definitions from JSON format
let columns_json: Vec<String> = serde_json::from_value(table_def.columns.clone()) let stored_columns: Vec<ColumnDefinition> = serde_json::from_value(table_def.columns.clone())
.map_err(|e| Status::internal(format!("Column parsing error: {}", e)))?; .map_err(|e| Status::internal(format!("Column parsing error: {}", e)))?;
// convert ColumnDefinition -> (name, sql_type) using the same map_field_type logic
let mut columns = Vec::new(); let mut columns = Vec::new();
for col_def in columns_json { for col_def in stored_columns {
let parts: Vec<&str> = col_def.splitn(2, ' ').collect(); let col_name = col_def.name.trim().to_string();
if parts.len() != 2 { let sql_type = map_field_type(&col_def.field_type)
return Err(Status::internal("Invalid column format")); .map_err(|e| Status::invalid_argument(format!("Invalid type for column '{}': {}", col_name, e)))?;
} columns.push((col_name, sql_type));
let name = parts[0].trim_matches('"').to_string();
let sql_type = parts[1].to_string();
columns.push((name, sql_type));
} }
// Build list of valid system columns (foreign keys and special columns) // Build list of valid system columns (foreign keys and special columns)

View File

@@ -5,6 +5,7 @@ use sqlx::{PgPool, Arguments, Row};
use sqlx::postgres::PgArguments; use sqlx::postgres::PgArguments;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use common::proto::komp_ac::tables_data::{PutTableDataRequest, PutTableDataResponse}; use common::proto::komp_ac::tables_data::{PutTableDataRequest, PutTableDataResponse};
use common::proto::komp_ac::table_definition::ColumnDefinition;
use std::sync::Arc; use std::sync::Arc;
use prost_types::value::Kind; use prost_types::value::Kind;
@@ -14,6 +15,7 @@ use std::collections::HashMap;
use crate::steel::server::execution::{self, Value}; use crate::steel::server::execution::{self, Value};
use crate::indexer::{IndexCommand, IndexCommandData}; use crate::indexer::{IndexCommand, IndexCommandData};
use crate::table_definition::models::map_field_type;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::error; use tracing::error;
@@ -56,19 +58,20 @@ pub async fn put_table_data(
.map_err(|e| Status::internal(format!("Table lookup error: {}", e)))? .map_err(|e| Status::internal(format!("Table lookup error: {}", e)))?
.ok_or_else(|| Status::not_found("Table not found"))?; .ok_or_else(|| Status::not_found("Table not found"))?;
// Parse column definitions from JSON format // Parse column definitions from JSON format (now ColumnDefinition objects)
let columns_json: Vec<String> = serde_json::from_value(table_def.columns.clone()) let stored_columns: Vec<ColumnDefinition> = serde_json::from_value(table_def.columns.clone())
.map_err(|e| Status::internal(format!("Column parsing error: {}", e)))?; .map_err(|e| Status::internal(format!("Column parsing error: {}", e)))?;
// Convert ColumnDefinition → (name, sql_type)
let mut columns = Vec::new(); let mut columns = Vec::new();
for col_def in columns_json { for col_def in stored_columns {
let parts: Vec<&str> = col_def.splitn(2, ' ').collect(); let col_name = col_def.name.trim().to_string();
if parts.len() != 2 { let sql_type = map_field_type(&col_def.field_type)
return Err(Status::internal("Invalid column format")); .map_err(|e| Status::invalid_argument(format!(
} "Invalid type for column '{}': {}",
let name = parts[0].trim_matches('"').to_string(); col_name, e
let sql_type = parts[1].to_string(); )))?;
columns.push((name, sql_type)); columns.push((col_name, sql_type));
} }
// Build list of valid system columns (foreign keys and special columns) // Build list of valid system columns (foreign keys and special columns)

View File

@@ -3,8 +3,8 @@
use crate::common::setup_isolated_db; use crate::common::setup_isolated_db;
use server::table_script::handlers::post_table_script::post_table_script; // Fixed import use server::table_script::handlers::post_table_script::post_table_script; // Fixed import
use common::proto::komp_ac::table_script::PostTableScriptRequest; use common::proto::komp_ac::table_script::PostTableScriptRequest;
use common::proto::komp_ac::table_definition::ColumnDefinition;
use rstest::*; use rstest::*;
use serde_json::json;
use sqlx::PgPool; use sqlx::PgPool;
/// Helper function to create a test table with specified columns /// Helper function to create a test table with specified columns
@@ -12,15 +12,10 @@ async fn create_test_table(
pool: &PgPool, pool: &PgPool,
schema_id: i64, schema_id: i64,
table_name: &str, table_name: &str,
columns: Vec<(&str, &str)>, columns: Vec<ColumnDefinition>,
) -> i64 { ) -> i64 {
let column_definitions: Vec<String> = columns let columns_json = serde_json::to_value(columns).unwrap();
.iter() let indexes_json = serde_json::json!([]);
.map(|(name, type_def)| format!("\"{}\" {}", name, type_def))
.collect();
let columns_json = json!(column_definitions);
let indexes_json = json!([]);
sqlx::query_scalar!( sqlx::query_scalar!(
r#"INSERT INTO table_definitions (schema_id, table_name, columns, indexes) r#"INSERT INTO table_definitions (schema_id, table_name, columns, indexes)
@@ -115,22 +110,17 @@ async fn test_comprehensive_error_scenarios(
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
// Create comprehensive error test table // Create comprehensive error test table
let columns = vec![ let columns: Vec<ColumnDefinition> = vec![
// Valid types ColumnDefinition { name: "valid_numeric".into(), field_type: "NUMERIC(10, 2)".into() },
("valid_numeric", "NUMERIC(10, 2)"), ColumnDefinition { name: "valid_integer".into(), field_type: "INTEGER".into() },
("valid_integer", "INTEGER"), ColumnDefinition { name: "text_col".into(), field_type: "TEXT".into() },
ColumnDefinition { name: "boolean_col".into(), field_type: "BOOLEAN".into() },
// Invalid for math operations ColumnDefinition { name: "bigint_col".into(), field_type: "BIGINT".into() },
("text_col", "TEXT"), ColumnDefinition { name: "date_col".into(), field_type: "DATE".into() },
("boolean_col", "BOOLEAN"), ColumnDefinition { name: "timestamp_col".into(), field_type: "TIMESTAMPTZ".into() },
("bigint_col", "BIGINT"), ColumnDefinition { name: "bigint_target".into(), field_type: "BIGINT".into() },
("date_col", "DATE"), ColumnDefinition { name: "date_target".into(), field_type: "DATE".into() },
("timestamp_col", "TIMESTAMPTZ"), ColumnDefinition { name: "timestamp_target".into(), field_type: "TIMESTAMPTZ".into() },
// Invalid target types
("bigint_target", "BIGINT"),
("date_target", "DATE"),
("timestamp_target", "TIMESTAMPTZ"),
]; ];
let table_id = create_test_table(&pool, schema_id, "error_table", columns).await; let table_id = create_test_table(&pool, schema_id, "error_table", columns).await;
@@ -169,7 +159,9 @@ async fn test_malformed_script_scenarios(
let pool = setup_isolated_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
let columns = vec![("result", "NUMERIC(10, 2)")]; let columns: Vec<ColumnDefinition> = vec![
ColumnDefinition { name: "result".into(), field_type: "NUMERIC(10, 2)".into() }
];
let table_id = create_test_table(&pool, schema_id, "malformed_test", columns).await; let table_id = create_test_table(&pool, schema_id, "malformed_test", columns).await;
let request = PostTableScriptRequest { let request = PostTableScriptRequest {
@@ -194,7 +186,9 @@ async fn test_advanced_validation_scenarios(
let pool = setup_isolated_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
let columns = vec![("result", "NUMERIC(10, 2)")]; let columns: Vec<ColumnDefinition> = vec![
ColumnDefinition { name: "result".into(), field_type: "NUMERIC(10, 2)".into() }
];
let table_id = create_test_table(&pool, schema_id, "advanced_test", columns).await; let table_id = create_test_table(&pool, schema_id, "advanced_test", columns).await;
let request = PostTableScriptRequest { let request = PostTableScriptRequest {
@@ -236,16 +230,16 @@ async fn test_dependency_cycle_detection() {
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
// Create table_b first // Create table_b first
let table_b_columns = vec![ let table_b_columns: Vec<ColumnDefinition> = vec![
("value_b", "NUMERIC(10, 2)"), ColumnDefinition { name: "value_b".into(), field_type: "NUMERIC(10, 2)".into() },
("result_b", "NUMERIC(10, 2)"), ColumnDefinition { name: "result_b".into(), field_type: "NUMERIC(10, 2)".into() },
]; ];
let table_b_id = create_test_table(&pool, schema_id, "table_b", table_b_columns).await; let table_b_id = create_test_table(&pool, schema_id, "table_b", table_b_columns).await;
// Create table_a // Create table_a
let table_a_columns = vec![ let table_a_columns: Vec<ColumnDefinition> = vec![
("value_a", "NUMERIC(10, 2)"), ColumnDefinition { name: "value_a".into(), field_type: "NUMERIC(10, 2)".into() },
("result_a", "NUMERIC(10, 2)"), ColumnDefinition { name: "result_a".into(), field_type: "NUMERIC(10, 2)".into() },
]; ];
let table_a_id = create_test_table(&pool, schema_id, "table_a", table_a_columns).await; let table_a_id = create_test_table(&pool, schema_id, "table_a", table_a_columns).await;
@@ -305,7 +299,9 @@ async fn test_edge_case_identifiers(
let pool = setup_isolated_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
let columns = vec![("result", "NUMERIC(10, 2)")]; let columns: Vec<ColumnDefinition> = vec![
ColumnDefinition { name: "result".into(), field_type: "NUMERIC(10, 2)".into() }
];
let table_id = create_test_table(&pool, schema_id, "identifier_test", columns).await; let table_id = create_test_table(&pool, schema_id, "identifier_test", columns).await;
// Test with edge case identifier in script // Test with edge case identifier in script
@@ -342,7 +338,9 @@ async fn test_sql_injection_prevention() {
let pool = setup_isolated_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
let columns = vec![("result", "NUMERIC(10, 2)")]; let columns: Vec<ColumnDefinition> = vec![
ColumnDefinition { name: "result".into(), field_type: "NUMERIC(10, 2)".into() }
];
let table_id = create_test_table(&pool, schema_id, "injection_test", columns).await; let table_id = create_test_table(&pool, schema_id, "injection_test", columns).await;
// Attempt SQL injection through script content // Attempt SQL injection through script content
@@ -388,9 +386,9 @@ async fn test_performance_with_deeply_nested_expressions() {
let pool = setup_isolated_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
let columns = vec![ let columns: Vec<ColumnDefinition> = vec![
("x", "NUMERIC(15, 8)"), ColumnDefinition { name: "x".into(), field_type: "NUMERIC(15, 8)".into() },
("performance_result", "NUMERIC(25, 12)"), ColumnDefinition { name: "performance_result".into(), field_type: "NUMERIC(25, 12)".into() },
]; ];
let table_id = create_test_table(&pool, schema_id, "performance_test", columns).await; let table_id = create_test_table(&pool, schema_id, "performance_test", columns).await;
@@ -437,11 +435,11 @@ async fn test_concurrent_script_creation() {
let pool = setup_isolated_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
let columns = vec![ let columns: Vec<ColumnDefinition> = vec![
("value", "NUMERIC(10, 2)"), ColumnDefinition { name: "value".into(), field_type: "NUMERIC(10, 2)".into() },
("result1", "NUMERIC(10, 2)"), ColumnDefinition { name: "result1".into(), field_type: "NUMERIC(10, 2)".into() },
("result2", "NUMERIC(10, 2)"), ColumnDefinition { name: "result2".into(), field_type: "NUMERIC(10, 2)".into() },
("result3", "NUMERIC(10, 2)"), ColumnDefinition { name: "result3".into(), field_type: "NUMERIC(10, 2)".into() },
]; ];
let table_id = create_test_table(&pool, schema_id, "concurrent_test", columns).await; let table_id = create_test_table(&pool, schema_id, "concurrent_test", columns).await;
@@ -500,9 +498,10 @@ async fn test_error_message_localization_and_clarity() {
let pool = setup_isolated_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
let columns = vec![
("text_col", "TEXT"), let columns: Vec<ColumnDefinition> = vec![
("result", "NUMERIC(10, 2)"), ColumnDefinition { name: "text_col".into(), field_type: "TEXT".into() },
ColumnDefinition { name: "result".into(), field_type: "NUMERIC(10, 2)".into() },
]; ];
let table_id = create_test_table(&pool, schema_id, "error_clarity_test", columns).await; let table_id = create_test_table(&pool, schema_id, "error_clarity_test", columns).await;

View File

@@ -3,8 +3,8 @@
use crate::common::setup_isolated_db; use crate::common::setup_isolated_db;
use server::table_script::handlers::post_table_script::post_table_script; // Fixed import use server::table_script::handlers::post_table_script::post_table_script; // Fixed import
use common::proto::komp_ac::table_script::PostTableScriptRequest; use common::proto::komp_ac::table_script::PostTableScriptRequest;
use common::proto::komp_ac::table_definition::ColumnDefinition;
use rstest::*; use rstest::*;
use serde_json::json;
use sqlx::PgPool; use sqlx::PgPool;
/// Helper function to create a test table with specified columns /// Helper function to create a test table with specified columns
@@ -12,15 +12,10 @@ async fn create_test_table(
pool: &PgPool, pool: &PgPool,
schema_id: i64, schema_id: i64,
table_name: &str, table_name: &str,
columns: Vec<(&str, &str)>, columns: Vec<ColumnDefinition>,
) -> i64 { ) -> i64 {
let column_definitions: Vec<String> = columns let columns_json = serde_json::to_value(columns).unwrap();
.iter() let indexes_json = serde_json::json!([]);
.map(|(name, type_def)| format!("\"{}\" {}", name, type_def))
.collect();
let columns_json = json!(column_definitions);
let indexes_json = json!([]);
sqlx::query_scalar!( sqlx::query_scalar!(
r#"INSERT INTO table_definitions (schema_id, table_name, columns, indexes) r#"INSERT INTO table_definitions (schema_id, table_name, columns, indexes)
@@ -97,7 +92,9 @@ async fn test_steel_decimal_literal_operations(
let pool = setup_isolated_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
let columns = vec![("result", "NUMERIC(30, 15)")]; let columns: Vec<ColumnDefinition> = vec![
ColumnDefinition { name: "result".to_string(), field_type: "NUMERIC(30, 15)".to_string() }
];
let table_id = create_test_table(&pool, schema_id, "literal_test", columns).await; let table_id = create_test_table(&pool, schema_id, "literal_test", columns).await;
let script = format!(r#"({} "{}" "{}")"#, operation, value1, value2); let script = format!(r#"({} "{}" "{}")"#, operation, value1, value2);
@@ -133,9 +130,9 @@ async fn test_steel_decimal_column_operations(
let pool = setup_isolated_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
let columns = vec![ let columns: Vec<ColumnDefinition> = vec![
("test_value", column_type), ColumnDefinition { name: "test_value".to_string(), field_type: column_type.to_string() },
("result", "NUMERIC(30, 15)"), ColumnDefinition { name: "result".to_string(), field_type: "NUMERIC(30, 15)".to_string() },
]; ];
let table_id = create_test_table(&pool, schema_id, "column_test", columns).await; let table_id = create_test_table(&pool, schema_id, "column_test", columns).await;
@@ -179,12 +176,12 @@ async fn test_complex_financial_calculation(
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
// Create a realistic financial calculation table // Create a realistic financial calculation table
let columns = vec![ let columns: Vec<ColumnDefinition> = vec![
("principal", "NUMERIC(16, 2)"), // Principal amount ColumnDefinition { name: "principal".to_string(), field_type: "NUMERIC(16, 2)".to_string() }, // Principal amount
("annual_rate", "NUMERIC(6, 5)"), // Interest rate ColumnDefinition { name: "annual_rate".to_string(), field_type: "NUMERIC(6, 5)".to_string() }, // Interest rate
("years", "INTEGER"), // Time period ColumnDefinition { name: "years".to_string(), field_type: "INTEGER".to_string() }, // Time period
("compounding_periods", "INTEGER"), // Compounding frequency ColumnDefinition { name: "compounding_periods".to_string(), field_type: "INTEGER".to_string() }, // Compounding frequency
("compound_interest", "NUMERIC(20, 8)"), // Result ColumnDefinition { name: "compound_interest".to_string(), field_type: "NUMERIC(20, 8)".to_string() }, // Result
]; ];
let table_id = create_test_table(&pool, schema_id, "financial_calc", columns).await; let table_id = create_test_table(&pool, schema_id, "financial_calc", columns).await;
@@ -217,11 +214,11 @@ async fn test_scientific_precision_calculations() {
let pool = setup_isolated_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
let columns = vec![ let columns: Vec<ColumnDefinition> = vec![
("measurement_a", "NUMERIC(25, 15)"), ColumnDefinition { name: "measurement_a".to_string(), field_type: "NUMERIC(25, 15)".to_string() },
("measurement_b", "NUMERIC(25, 15)"), ColumnDefinition { name: "measurement_b".to_string(), field_type: "NUMERIC(25, 15)".to_string() },
("coefficient", "NUMERIC(10, 8)"), ColumnDefinition { name: "coefficient".to_string(), field_type: "NUMERIC(10, 8)".to_string() },
("scientific_result", "NUMERIC(30, 18)"), ColumnDefinition { name: "scientific_result".to_string(), field_type: "NUMERIC(30, 18)".to_string() },
]; ];
let table_id = create_test_table(&pool, schema_id, "scientific_data", columns).await; let table_id = create_test_table(&pool, schema_id, "scientific_data", columns).await;
@@ -259,9 +256,9 @@ async fn test_precision_boundary_conditions(
let pool = setup_isolated_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
let columns = vec![ let columns: Vec<ColumnDefinition> = vec![
("boundary_value", numeric_type), ColumnDefinition { name: "boundary_value".to_string(), field_type: numeric_type.to_string() },
("result", "NUMERIC(30, 15)"), ColumnDefinition { name: "result".to_string(), field_type: "NUMERIC(30, 15)".to_string() },
]; ];
let table_id = create_test_table(&pool, schema_id, "boundary_test", columns).await; let table_id = create_test_table(&pool, schema_id, "boundary_test", columns).await;
@@ -284,11 +281,11 @@ async fn test_mixed_integer_and_numeric_operations() {
let pool = setup_isolated_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
let columns = vec![ let columns: Vec<ColumnDefinition> = vec![
("integer_quantity", "INTEGER"), ColumnDefinition { name: "integer_quantity".to_string(), field_type: "INTEGER".to_string() },
("numeric_price", "NUMERIC(10, 4)"), ColumnDefinition { name: "numeric_price".to_string(), field_type: "NUMERIC(10, 4)".to_string() },
("numeric_tax_rate", "NUMERIC(5, 4)"), ColumnDefinition { name: "numeric_tax_rate".to_string(), field_type: "NUMERIC(5, 4)".to_string() },
("total_with_tax", "NUMERIC(15, 4)"), ColumnDefinition { name: "total_with_tax".to_string(), field_type: "NUMERIC(15, 4)".to_string() },
]; ];
let table_id = create_test_table(&pool, schema_id, "mixed_types_calc", columns).await; let table_id = create_test_table(&pool, schema_id, "mixed_types_calc", columns).await;
@@ -325,9 +322,9 @@ async fn test_mathematical_edge_cases(
let pool = setup_isolated_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
let columns = vec![ let columns: Vec<ColumnDefinition> = vec![
("test_value", "NUMERIC(15, 6)"), ColumnDefinition { name: "test_value".to_string(), field_type: "NUMERIC(15, 6)".to_string() },
("result", "NUMERIC(20, 8)"), ColumnDefinition { name: "result".to_string(), field_type: "NUMERIC(20, 8)".to_string() },
]; ];
let table_id = create_test_table(&pool, schema_id, "edge_case_test", columns).await; let table_id = create_test_table(&pool, schema_id, "edge_case_test", columns).await;
@@ -381,10 +378,10 @@ async fn test_comparison_operations_with_valid_types() {
let pool = setup_isolated_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
let columns = vec![ let columns: Vec<ColumnDefinition> = vec![
("value_a", "NUMERIC(10, 2)"), ColumnDefinition { name: "value_a".to_string(), field_type: "NUMERIC(10, 2)".to_string() },
("value_b", "INTEGER"), ColumnDefinition { name: "value_b".to_string(), field_type: "INTEGER".to_string() },
("comparison_result", "BOOLEAN"), ColumnDefinition { name: "comparison_result".to_string(), field_type: "BOOLEAN".to_string() },
]; ];
let table_id = create_test_table(&pool, schema_id, "comparison_test", columns).await; let table_id = create_test_table(&pool, schema_id, "comparison_test", columns).await;
@@ -419,11 +416,11 @@ async fn test_nested_mathematical_expressions() {
let pool = setup_isolated_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
let columns = vec![ let columns: Vec<ColumnDefinition> = vec![
("x", "NUMERIC(15, 8)"), ColumnDefinition { name: "x".to_string(), field_type: "NUMERIC(15, 8)".to_string() },
("y", "NUMERIC(15, 8)"), ColumnDefinition { name: "y".to_string(), field_type: "NUMERIC(15, 8)".to_string() },
("z", "INTEGER"), ColumnDefinition { name: "z".to_string(), field_type: "INTEGER".to_string() },
("nested_result", "NUMERIC(25, 12)"), ColumnDefinition { name: "nested_result".to_string(), field_type: "NUMERIC(25, 12)".to_string() },
]; ];
let table_id = create_test_table(&pool, schema_id, "nested_calc", columns).await; let table_id = create_test_table(&pool, schema_id, "nested_calc", columns).await;

View File

@@ -3,7 +3,7 @@
use crate::common::setup_isolated_db; use crate::common::setup_isolated_db;
use server::table_script::handlers::post_table_script::post_table_script; use server::table_script::handlers::post_table_script::post_table_script;
use common::proto::komp_ac::table_script::{PostTableScriptRequest, TableScriptResponse}; use common::proto::komp_ac::table_script::{PostTableScriptRequest, TableScriptResponse};
use serde_json::json; use common::proto::komp_ac::table_definition::ColumnDefinition;
use sqlx::PgPool; use sqlx::PgPool;
/// Test utilities for table script integration testing - moved to top level for shared access /// Test utilities for table script integration testing - moved to top level for shared access
@@ -26,14 +26,9 @@ impl TableScriptTestHelper {
} }
} }
pub async fn create_table_with_types(&self, table_name: &str, column_definitions: Vec<(&str, &str)>) -> i64 { pub async fn create_table_with_types(&self, table_name: &str, column_definitions: Vec<ColumnDefinition>) -> i64 {
let columns: Vec<String> = column_definitions let columns_json = serde_json::to_value(column_definitions).unwrap();
.iter() let indexes_json = serde_json::json!([]);
.map(|(name, type_def)| format!("\"{}\" {}", name, type_def))
.collect();
let columns_json = json!(columns);
let indexes_json = json!([]);
sqlx::query_scalar!( sqlx::query_scalar!(
r#"INSERT INTO table_definitions (schema_id, table_name, columns, indexes) r#"INSERT INTO table_definitions (schema_id, table_name, columns, indexes)
@@ -73,24 +68,24 @@ mod integration_tests {
"comprehensive_table", "comprehensive_table",
vec![ vec![
// Supported types for math operations // Supported types for math operations
("integer_col", "INTEGER"), ColumnDefinition { name: "integer_col".to_string(), field_type: "INTEGER".to_string() },
("numeric_basic", "NUMERIC(10, 2)"), ColumnDefinition { name: "numeric_basic".to_string(), field_type: "NUMERIC(10, 2)".to_string() },
("numeric_high_precision", "NUMERIC(28, 15)"), ColumnDefinition { name: "numeric_high_precision".to_string(), field_type: "NUMERIC(28, 15)".to_string() },
("numeric_currency", "NUMERIC(14, 4)"), ColumnDefinition { name: "numeric_currency".to_string(), field_type: "NUMERIC(14, 4)".to_string() },
// Supported but not for math operations // Supported but not for math operations
("text_col", "TEXT"), ColumnDefinition { name: "text_col".to_string(), field_type: "TEXT".to_string() },
("boolean_col", "BOOLEAN"), ColumnDefinition { name: "boolean_col".to_string(), field_type: "BOOLEAN".to_string() },
// Prohibited types entirely // Prohibited types entirely
("bigint_col", "BIGINT"), ColumnDefinition { name: "bigint_col".to_string(), field_type: "BIGINT".to_string() },
("date_col", "DATE"), ColumnDefinition { name: "date_col".to_string(), field_type: "DATE".to_string() },
("timestamp_col", "TIMESTAMPTZ"), ColumnDefinition { name: "timestamp_col".to_string(), field_type: "TIMESTAMPTZ".to_string() },
// Result columns of various types // Result columns of various types
("result_integer", "INTEGER"), ColumnDefinition { name: "result_integer".to_string(), field_type: "INTEGER".to_string() },
("result_numeric", "NUMERIC(15, 5)"), ColumnDefinition { name: "result_numeric".to_string(), field_type: "NUMERIC(15, 5)".to_string() },
("result_text", "TEXT"), ColumnDefinition { name: "result_text".to_string(), field_type: "TEXT".to_string() },
] ]
).await; ).await;
@@ -150,13 +145,13 @@ mod integration_tests {
let table_id = helper.create_table_with_types( let table_id = helper.create_table_with_types(
"precision_table", "precision_table",
vec![ vec![
("low_precision", "NUMERIC(5, 2)"), // e.g., 999.99 ColumnDefinition { name: "low_precision".to_string(), field_type: "NUMERIC(5, 2)".to_string() }, // e.g., 999.99
("medium_precision", "NUMERIC(10, 4)"), // e.g., 999999.9999 ColumnDefinition { name: "medium_precision".to_string(), field_type: "NUMERIC(10, 4)".to_string() }, // e.g., 999999.9999
("high_precision", "NUMERIC(28, 15)"), // Maximum PostgreSQL precision ColumnDefinition { name: "high_precision".to_string(), field_type: "NUMERIC(28, 15)".to_string() }, // Maximum PostgreSQL precision
("currency", "NUMERIC(14, 4)"), // Standard currency precision ColumnDefinition { name: "currency".to_string(), field_type: "NUMERIC(14, 4)".to_string() }, // Standard currency precision
("percentage", "NUMERIC(5, 4)"), // e.g., 0.9999 (99.99%) ColumnDefinition { name: "percentage".to_string(), field_type: "NUMERIC(5, 4)".to_string() }, // e.g., 0.9999 (99.99%)
("integer_val", "INTEGER"), ColumnDefinition { name: "integer_val".to_string(), field_type: "INTEGER".to_string() },
("result", "NUMERIC(30, 15)"), ColumnDefinition { name: "result".to_string(), field_type: "NUMERIC(30, 15)".to_string() },
] ]
).await; ).await;
@@ -202,12 +197,12 @@ mod integration_tests {
let table_id = helper.create_table_with_types( let table_id = helper.create_table_with_types(
"financial_instruments", "financial_instruments",
vec![ vec![
("principal", "NUMERIC(16, 2)"), // Principal amount ColumnDefinition { name: "principal".to_string(), field_type: "NUMERIC(16, 2)".to_string() }, // Principal amount
("annual_rate", "NUMERIC(6, 5)"), // Interest rate (e.g., 0.05250) ColumnDefinition { name: "annual_rate".to_string(), field_type: "NUMERIC(6, 5)".to_string() }, // Interest rate (e.g., 0.05250)
("years", "INTEGER"), // Time period ColumnDefinition { name: "years".to_string(), field_type: "INTEGER".to_string() }, // Time period
("compounding_periods", "INTEGER"), // Compounding frequency ColumnDefinition { name: "compounding_periods".to_string(), field_type: "INTEGER".to_string() }, // Compounding frequency
("fees", "NUMERIC(10, 2)"), // Transaction fees ColumnDefinition { name: "fees".to_string(), field_type: "NUMERIC(10, 2)".to_string() }, // Transaction fees
("compound_interest", "NUMERIC(20, 8)"), // Result column ColumnDefinition { name: "compound_interest".to_string(), field_type: "NUMERIC(20, 8)".to_string() }, // Result column
] ]
).await; ).await;
@@ -237,9 +232,9 @@ mod integration_tests {
let table_id = helper.create_table_with_types( let table_id = helper.create_table_with_types(
"scientific_data", "scientific_data",
vec![ vec![
("large_number", "NUMERIC(30, 10)"), ColumnDefinition { name: "large_number".to_string(), field_type: "NUMERIC(30, 10)".to_string() },
("small_number", "NUMERIC(30, 20)"), ColumnDefinition { name: "small_number".to_string(), field_type: "NUMERIC(30, 20)".to_string() },
("result", "NUMERIC(35, 25)"), ColumnDefinition { name: "result".to_string(), field_type: "NUMERIC(35, 25)".to_string() },
] ]
).await; ).await;
@@ -265,8 +260,8 @@ mod integration_tests {
let table_a_id = helper.create_table_with_types( let table_a_id = helper.create_table_with_types(
"table_a", "table_a",
vec![ vec![
("value_a", "NUMERIC(10, 2)"), ColumnDefinition { name: "value_a".to_string(), field_type: "NUMERIC(10, 2)".to_string() },
("result_a", "NUMERIC(10, 2)"), ColumnDefinition { name: "result_a".to_string(), field_type: "NUMERIC(10, 2)".to_string() },
] ]
).await; ).await;
println!("Created table_a with ID: {}", table_a_id); println!("Created table_a with ID: {}", table_a_id);
@@ -274,8 +269,8 @@ mod integration_tests {
let table_b_id = helper.create_table_with_types( let table_b_id = helper.create_table_with_types(
"table_b", "table_b",
vec![ vec![
("value_b", "NUMERIC(10, 2)"), ColumnDefinition { name: "value_b".to_string(), field_type: "NUMERIC(10, 2)".to_string() },
("result_b", "NUMERIC(10, 2)"), ColumnDefinition { name: "result_b".to_string(), field_type: "NUMERIC(10, 2)".to_string() },
] ]
).await; ).await;
println!("Created table_b with ID: {}", table_b_id); println!("Created table_b with ID: {}", table_b_id);
@@ -354,10 +349,10 @@ mod integration_tests {
let table_id = helper.create_table_with_types( let table_id = helper.create_table_with_types(
"error_test_table", "error_test_table",
vec![ vec![
("text_field", "TEXT"), ColumnDefinition { name: "text_field".to_string(), field_type: "TEXT".to_string() },
("numeric_field", "NUMERIC(10, 2)"), ColumnDefinition { name: "numeric_field".to_string(), field_type: "NUMERIC(10, 2)".to_string() },
("boolean_field", "BOOLEAN"), ColumnDefinition { name: "boolean_field".to_string(), field_type: "BOOLEAN".to_string() },
("bigint_field", "BIGINT"), ColumnDefinition { name: "bigint_field".to_string(), field_type: "BIGINT".to_string() },
] ]
).await; ).await;
@@ -417,11 +412,11 @@ mod integration_tests {
let table_id = helper.create_table_with_types( let table_id = helper.create_table_with_types(
"performance_table", "performance_table",
vec![ vec![
("x", "NUMERIC(15, 8)"), ColumnDefinition { name: "x".to_string(), field_type: "NUMERIC(15, 8)".to_string() },
("y", "NUMERIC(15, 8)"), ColumnDefinition { name: "y".to_string(), field_type: "NUMERIC(15, 8)".to_string() },
("z", "NUMERIC(15, 8)"), ColumnDefinition { name: "z".to_string(), field_type: "NUMERIC(15, 8)".to_string() },
("w", "NUMERIC(15, 8)"), ColumnDefinition { name: "w".to_string(), field_type: "NUMERIC(15, 8)".to_string() },
("complex_result", "NUMERIC(25, 12)"), ColumnDefinition { name: "complex_result".to_string(), field_type: "NUMERIC(25, 12)".to_string() },
] ]
).await; ).await;
@@ -456,11 +451,11 @@ mod integration_tests {
let table_id = helper.create_table_with_types( let table_id = helper.create_table_with_types(
"boundary_table", "boundary_table",
vec![ vec![
("min_numeric", "NUMERIC(1, 0)"), // Minimum: single digit, no decimal ColumnDefinition { name: "min_numeric".to_string(), field_type: "NUMERIC(1, 0)".to_string() }, // Minimum: single digit, no decimal
("max_numeric", "NUMERIC(1000, 999)"), // Maximum PostgreSQL allows ColumnDefinition { name: "max_numeric".to_string(), field_type: "NUMERIC(1000, 999)".to_string() }, // Maximum PostgreSQL allows
("zero_scale", "NUMERIC(10, 0)"), // Integer-like numeric ColumnDefinition { name: "zero_scale".to_string(), field_type: "NUMERIC(10, 0)".to_string() }, // Integer-like numeric
("max_scale", "NUMERIC(28, 28)"), // Maximum scale ColumnDefinition { name: "max_scale".to_string(), field_type: "NUMERIC(28, 28)".to_string() }, // Maximum scale
("result", "NUMERIC(1000, 999)"), ColumnDefinition { name: "result".to_string(), field_type: "NUMERIC(1000, 999)".to_string() },
] ]
).await; ).await;
@@ -495,10 +490,10 @@ mod steel_decimal_integration_tests {
let table_id = helper.create_table_with_types( let table_id = helper.create_table_with_types(
"test_execution_table", "test_execution_table",
vec![ vec![
("amount", "NUMERIC(10, 2)"), ColumnDefinition { name: "amount".to_string(), field_type: "NUMERIC(10, 2)".to_string() },
("quantity", "INTEGER"), ColumnDefinition { name: "quantity".to_string(), field_type: "INTEGER".to_string() },
("tax_rate", "NUMERIC(5, 4)"), ColumnDefinition { name: "tax_rate".to_string(), field_type: "NUMERIC(5, 4)".to_string() },
("result", "NUMERIC(15, 4)"), // Add a result column ColumnDefinition { name: "result".to_string(), field_type: "NUMERIC(15, 4)".to_string() }, // Add a result column
] ]
).await; ).await;
println!("Created test table with ID: {}", table_id); println!("Created test table with ID: {}", table_id);
@@ -575,9 +570,9 @@ mod steel_decimal_integration_tests {
let table_id = helper.create_table_with_types( let table_id = helper.create_table_with_types(
"precision_test_table", "precision_test_table",
vec![ vec![
("precise_value", "NUMERIC(20, 12)"), ColumnDefinition { name: "precise_value".to_string(), field_type: "NUMERIC(20, 12)".to_string() },
("multiplier", "NUMERIC(20, 12)"), ColumnDefinition { name: "multiplier".to_string(), field_type: "NUMERIC(20, 12)".to_string() },
("result", "NUMERIC(25, 15)"), // Add result column ColumnDefinition { name: "result".to_string(), field_type: "NUMERIC(25, 15)".to_string() }, // Add result column
] ]
).await; ).await;
println!("Created precision test table with ID: {}", table_id); println!("Created precision test table with ID: {}", table_id);

View File

@@ -3,7 +3,7 @@
use crate::common::setup_isolated_db; use crate::common::setup_isolated_db;
use server::table_script::handlers::post_table_script::post_table_script; use server::table_script::handlers::post_table_script::post_table_script;
use common::proto::komp_ac::table_script::PostTableScriptRequest; use common::proto::komp_ac::table_script::PostTableScriptRequest;
use serde_json::json; use common::proto::komp_ac::table_definition::ColumnDefinition;
use sqlx::PgPool; use sqlx::PgPool;
/// Helper function to create a test table with specified columns /// Helper function to create a test table with specified columns
@@ -11,15 +11,10 @@ async fn create_test_table(
pool: &PgPool, pool: &PgPool,
schema_id: i64, schema_id: i64,
table_name: &str, table_name: &str,
columns: Vec<(&str, &str)>, columns: Vec<ColumnDefinition>,
) -> i64 { ) -> i64 {
let column_definitions: Vec<String> = columns let columns_json = serde_json::to_value(columns).unwrap();
.iter() let indexes_json = serde_json::json!([]);
.map(|(name, type_def)| format!("\"{}\" {}", name, type_def))
.collect();
let columns_json = json!(column_definitions);
let indexes_json = json!([]);
sqlx::query_scalar!( sqlx::query_scalar!(
r#"INSERT INTO table_definitions (schema_id, table_name, columns, indexes) r#"INSERT INTO table_definitions (schema_id, table_name, columns, indexes)
@@ -67,7 +62,10 @@ async fn test_reject_bigint_target_column() {
&pool, &pool,
schema_id, schema_id,
"bigint_table", "bigint_table",
vec![("name", "TEXT"), ("big_number", "BIGINT")] vec![
ColumnDefinition { name: "name".to_string(), field_type: "TEXT".to_string() },
ColumnDefinition { name: "big_number".to_string(), field_type: "BIGINT".to_string() }
]
).await; ).await;
let request = PostTableScriptRequest { let request = PostTableScriptRequest {
@@ -99,7 +97,10 @@ async fn test_reject_date_target_column() {
&pool, &pool,
schema_id, schema_id,
"date_table", "date_table",
vec![("name", "TEXT"), ("event_date", "DATE")] vec![
ColumnDefinition { name: "name".to_string(), field_type: "TEXT".to_string() },
ColumnDefinition { name: "event_date".to_string(), field_type: "DATE".to_string() }
]
).await; ).await;
let request = PostTableScriptRequest { let request = PostTableScriptRequest {
@@ -131,7 +132,10 @@ async fn test_reject_timestamptz_target_column() {
&pool, &pool,
schema_id, schema_id,
"timestamp_table", "timestamp_table",
vec![("name", "TEXT"), ("created_time", "TIMESTAMPTZ")] vec![
ColumnDefinition { name: "name".to_string(), field_type: "TEXT".to_string() },
ColumnDefinition { name: "created_time".to_string(), field_type: "TIMESTAMPTZ".to_string() }
]
).await; ).await;
let request = PostTableScriptRequest { let request = PostTableScriptRequest {
@@ -164,9 +168,9 @@ async fn test_reject_text_in_mathematical_operations() {
schema_id, schema_id,
"text_math_table", "text_math_table",
vec![ vec![
("description", "TEXT"), ColumnDefinition { name: "description".to_string(), field_type: "TEXT".to_string() },
("amount", "NUMERIC(10, 2)"), ColumnDefinition { name: "amount".to_string(), field_type: "NUMERIC(10, 2)".to_string() },
("result", "NUMERIC(10, 2)") ColumnDefinition { name: "result".to_string(), field_type: "NUMERIC(10, 2)".to_string() }
] ]
).await; ).await;
@@ -202,9 +206,9 @@ async fn test_reject_boolean_in_mathematical_operations() {
schema_id, schema_id,
"boolean_math_table", "boolean_math_table",
vec![ vec![
("is_active", "BOOLEAN"), ColumnDefinition { name: "is_active".to_string(), field_type: "BOOLEAN".to_string() },
("amount", "NUMERIC(10, 2)"), ColumnDefinition { name: "amount".to_string(), field_type: "NUMERIC(10, 2)".to_string() },
("result", "NUMERIC(10, 2)") ColumnDefinition { name: "result".to_string(), field_type: "NUMERIC(10, 2)".to_string() }
] ]
).await; ).await;
@@ -240,8 +244,8 @@ async fn test_reject_bigint_in_mathematical_operations() {
schema_id, schema_id,
"bigint_math_table", "bigint_math_table",
vec![ vec![
("big_value", "BIGINT"), ColumnDefinition { name: "big_value".to_string(), field_type: "BIGINT".to_string() },
("result", "NUMERIC(10, 2)") ColumnDefinition { name: "result".to_string(), field_type: "NUMERIC(10, 2)".to_string() }
] ]
).await; ).await;
@@ -277,10 +281,10 @@ async fn test_allow_valid_script_with_allowed_types() {
schema_id, schema_id,
"allowed_types_table", "allowed_types_table",
vec![ vec![
("name", "TEXT"), ColumnDefinition { name: "name".to_string(), field_type: "TEXT".to_string() },
("count", "INTEGER"), ColumnDefinition { name: "count".to_string(), field_type: "INTEGER".to_string() },
("amount", "NUMERIC(10, 2)"), ColumnDefinition { name: "amount".to_string(), field_type: "NUMERIC(10, 2)".to_string() },
("computed_value", "TEXT") ColumnDefinition { name: "computed_value".to_string(), field_type: "TEXT".to_string() }
] ]
).await; ).await;
@@ -312,9 +316,9 @@ async fn test_allow_integer_and_numeric_in_math_operations() {
schema_id, schema_id,
"math_allowed_table", "math_allowed_table",
vec![ vec![
("quantity", "INTEGER"), ColumnDefinition { name: "quantity".to_string(), field_type: "INTEGER".to_string() },
("price", "NUMERIC(10, 2)"), ColumnDefinition { name: "price".to_string(), field_type: "NUMERIC(10, 2)".to_string() },
("total", "NUMERIC(12, 2)") ColumnDefinition { name: "total".to_string(), field_type: "NUMERIC(12, 2)".to_string() }
] ]
).await; ).await;
@@ -363,14 +367,19 @@ async fn test_script_without_table_links_should_fail() {
&pool, &pool,
schema_id, schema_id,
"table_a", "table_a",
vec![("value_a", "INTEGER"), ("result", "INTEGER")] vec![
ColumnDefinition { name: "value_a".to_string(), field_type: "INTEGER".to_string() },
ColumnDefinition { name: "result".to_string(), field_type: "INTEGER".to_string() }
]
).await; ).await;
let _table_b_id = create_test_table( let _table_b_id = create_test_table(
&pool, &pool,
schema_id, schema_id,
"table_b", "table_b",
vec![("value_b", "INTEGER")] vec![
ColumnDefinition { name: "value_b".to_string(), field_type: "INTEGER".to_string() }
]
).await; ).await;
// DON'T create a link between the tables // DON'T create a link between the tables
@@ -404,14 +413,19 @@ async fn test_script_with_table_links_should_succeed() {
&pool, &pool,
schema_id, schema_id,
"linked_table_a", "linked_table_a",
vec![("value_a", "INTEGER"), ("result", "INTEGER")] vec![
ColumnDefinition { name: "value_a".to_string(), field_type: "INTEGER".to_string() },
ColumnDefinition { name: "result".to_string(), field_type: "INTEGER".to_string() }
]
).await; ).await;
let table_b_id = create_test_table( let table_b_id = create_test_table(
&pool, &pool,
schema_id, schema_id,
"linked_table_b", "linked_table_b",
vec![("value_b", "INTEGER")] vec![
ColumnDefinition { name: "value_b".to_string(), field_type: "INTEGER".to_string() }
]
).await; ).await;
// Create a link between the tables (table_a can access table_b) // Create a link between the tables (table_a can access table_b)

View File

@@ -3,8 +3,8 @@
use crate::common::setup_isolated_db; use crate::common::setup_isolated_db;
use server::table_script::handlers::post_table_script::post_table_script; use server::table_script::handlers::post_table_script::post_table_script;
use common::proto::komp_ac::table_script::PostTableScriptRequest; use common::proto::komp_ac::table_script::PostTableScriptRequest;
use common::proto::komp_ac::table_definition::ColumnDefinition;
use rstest::*; use rstest::*;
use serde_json::json;
use sqlx::PgPool; use sqlx::PgPool;
/// Test fixture for allowed mathematical types /// Test fixture for allowed mathematical types
@@ -76,15 +76,10 @@ async fn create_test_table(
pool: &PgPool, pool: &PgPool,
schema_id: i64, schema_id: i64,
table_name: &str, table_name: &str,
columns: Vec<(&str, &str)>, columns: Vec<ColumnDefinition>,
) -> i64 { ) -> i64 {
let column_definitions: Vec<String> = columns let columns_json = serde_json::to_value(columns).unwrap();
.iter() let indexes_json = serde_json::json!([]);
.map(|(name, type_def)| format!("\"{}\" {}", name, type_def))
.collect();
let columns_json = json!(column_definitions);
let indexes_json = json!([]);
sqlx::query_scalar!( sqlx::query_scalar!(
r#"INSERT INTO table_definitions (schema_id, table_name, columns, indexes) r#"INSERT INTO table_definitions (schema_id, table_name, columns, indexes)
@@ -123,8 +118,17 @@ async fn test_allowed_types_in_math_operations(
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
// Create table with all allowed mathematical types plus a result column // Create table with all allowed mathematical types plus a result column
let mut columns = allowed_math_types.clone(); let mut columns: Vec<ColumnDefinition> = allowed_math_types
columns.push(("result", "NUMERIC(30, 15)")); .iter()
.map(|(name, field_type)| ColumnDefinition {
name: name.to_string(),
field_type: field_type.to_string(),
})
.collect();
columns.push(ColumnDefinition {
name: "result".to_string(),
field_type: "NUMERIC(30, 15)".to_string(),
});
let table_id = create_test_table(&pool, schema_id, "math_test_table", columns).await; let table_id = create_test_table(&pool, schema_id, "math_test_table", columns).await;
@@ -172,8 +176,17 @@ async fn test_prohibited_types_in_math_operations(
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
// Create table with prohibited types plus a valid result column // Create table with prohibited types plus a valid result column
let mut columns = prohibited_math_types.clone(); let mut columns: Vec<ColumnDefinition> = prohibited_math_types
columns.push(("result", "NUMERIC(15, 6)")); .iter()
.map(|(name, field_type)| ColumnDefinition {
name: name.to_string(),
field_type: field_type.to_string(),
})
.collect();
columns.push(ColumnDefinition {
name: "result".to_string(),
field_type: "NUMERIC(15, 6)".to_string(),
});
let table_id = create_test_table(&pool, schema_id, "prohibited_math_table", columns).await; let table_id = create_test_table(&pool, schema_id, "prohibited_math_table", columns).await;
@@ -225,8 +238,17 @@ async fn test_prohibited_target_column_types(
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
// Create table with prohibited target types plus some valid source columns // Create table with prohibited target types plus some valid source columns
let mut columns = prohibited_target_types.clone(); let mut columns: Vec<ColumnDefinition> = prohibited_target_types
columns.push(("amount", "NUMERIC(10, 2)")); .iter()
.map(|(name, field_type)| ColumnDefinition {
name: name.to_string(),
field_type: field_type.to_string(),
})
.collect();
columns.push(ColumnDefinition {
name: "amount".to_string(),
field_type: "NUMERIC(10, 2)".to_string(),
});
let table_id = create_test_table(&pool, schema_id, "prohibited_target_table", columns).await; let table_id = create_test_table(&pool, schema_id, "prohibited_target_table", columns).await;
@@ -261,7 +283,12 @@ async fn test_system_column_restrictions(#[case] target_column: &str, #[case] de
let pool = setup_isolated_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
let columns = vec![("amount", "NUMERIC(10, 2)")]; let columns: Vec<ColumnDefinition> = vec![
ColumnDefinition {
name: "amount".to_string(),
field_type: "NUMERIC(10, 2)".to_string(),
}
];
let table_id = create_test_table(&pool, schema_id, "system_test_table", columns).await; let table_id = create_test_table(&pool, schema_id, "system_test_table", columns).await;
let script = r#"(+ "10" "20")"#; let script = r#"(+ "10" "20")"#;
@@ -290,22 +317,22 @@ async fn test_comprehensive_type_matrix() {
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
// Create comprehensive table with all type categories // Create comprehensive table with all type categories
let all_columns = vec![ let all_columns: Vec<ColumnDefinition> = vec![
// Allowed math types // Allowed math types
("integer_col", "INTEGER"), ColumnDefinition { name: "integer_col".to_string(), field_type: "INTEGER".to_string() },
("numeric_col", "NUMERIC(10, 2)"), ColumnDefinition { name: "numeric_col".to_string(), field_type: "NUMERIC(10, 2)".to_string() },
("high_precision", "NUMERIC(28, 15)"), ColumnDefinition { name: "high_precision".to_string(), field_type: "NUMERIC(28, 15)".to_string() },
// Prohibited math types // Prohibited math types
("text_col", "TEXT"), ColumnDefinition { name: "text_col".to_string(), field_type: "TEXT".to_string() },
("boolean_col", "BOOLEAN"), ColumnDefinition { name: "boolean_col".to_string(), field_type: "BOOLEAN".to_string() },
("bigint_col", "BIGINT"), ColumnDefinition { name: "bigint_col".to_string(), field_type: "BIGINT".to_string() },
("date_col", "DATE"), ColumnDefinition { name: "date_col".to_string(), field_type: "DATE".to_string() },
("timestamp_col", "TIMESTAMPTZ"), ColumnDefinition { name: "timestamp_col".to_string(), field_type: "TIMESTAMPTZ".to_string() },
// Result columns // Result columns
("result_numeric", "NUMERIC(20, 8)"), ColumnDefinition { name: "result_numeric".to_string(), field_type: "NUMERIC(20, 8)".to_string() },
("result_text", "TEXT"), ColumnDefinition { name: "result_text".to_string(), field_type: "TEXT".to_string() },
]; ];
let table_id = create_test_table(&pool, schema_id, "comprehensive_table", all_columns).await; let table_id = create_test_table(&pool, schema_id, "comprehensive_table", all_columns).await;
@@ -361,11 +388,11 @@ async fn test_complex_mathematical_expressions() {
let pool = setup_isolated_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
let columns = vec![ let columns: Vec<ColumnDefinition> = vec![
("principal", "NUMERIC(16, 2)"), ColumnDefinition { name: "principal".to_string(), field_type: "NUMERIC(16, 2)".to_string() },
("rate", "NUMERIC(6, 5)"), ColumnDefinition { name: "rate".to_string(), field_type: "NUMERIC(6, 5)".to_string() },
("years", "INTEGER"), ColumnDefinition { name: "years".to_string(), field_type: "INTEGER".to_string() },
("compound_result", "NUMERIC(20, 8)"), ColumnDefinition { name: "compound_result".to_string(), field_type: "NUMERIC(20, 8)".to_string() },
]; ];
let table_id = create_test_table(&pool, schema_id, "financial_table", columns).await; let table_id = create_test_table(&pool, schema_id, "financial_table", columns).await;
@@ -395,9 +422,9 @@ async fn test_nonexistent_column_reference() {
let pool = setup_isolated_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
let columns = vec![ let columns: Vec<ColumnDefinition> = vec![
("amount", "NUMERIC(10, 2)"), ColumnDefinition { name: "amount".to_string(), field_type: "NUMERIC(10, 2)".to_string() },
("result", "NUMERIC(10, 2)"), ColumnDefinition { name: "result".to_string(), field_type: "NUMERIC(10, 2)".to_string() },
]; ];
let table_id = create_test_table(&pool, schema_id, "simple_table", columns).await; let table_id = create_test_table(&pool, schema_id, "simple_table", columns).await;
@@ -427,9 +454,9 @@ async fn test_nonexistent_table_reference() {
let pool = setup_isolated_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await; let schema_id = get_default_schema_id(&pool).await;
let columns = vec![ let columns: Vec<ColumnDefinition> = vec![
("amount", "NUMERIC(10, 2)"), ColumnDefinition { name: "amount".to_string(), field_type: "NUMERIC(10, 2)".to_string() },
("result", "NUMERIC(10, 2)"), ColumnDefinition { name: "result".to_string(), field_type: "NUMERIC(10, 2)".to_string() },
]; ];
let table_id = create_test_table(&pool, schema_id, "existing_table", columns).await; let table_id = create_test_table(&pool, schema_id, "existing_table", columns).await;

View File

@@ -5,7 +5,6 @@ use sqlx::{PgPool, Row};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::{mpsc, Mutex}; use tokio::sync::{mpsc, Mutex};
use serde_json::json;
use chrono::Utc; use chrono::Utc;
use futures::future::join_all; use futures::future::join_all;
use prost_types::{value::Kind, Value}; use prost_types::{value::Kind, Value};
@@ -17,7 +16,7 @@ use common::proto::komp_ac::table_definition::{
PostTableDefinitionRequest, ColumnDefinition as TableColumnDefinition, TableLink, PostTableDefinitionRequest, ColumnDefinition as TableColumnDefinition, TableLink,
}; };
use common::proto::komp_ac::tables_data::{ use common::proto::komp_ac::tables_data::{
DeleteTableDataRequest, DeleteTableDataResponse, PostTableDataRequest, PutTableDataRequest, DeleteTableDataRequest, PostTableDataRequest, PutTableDataRequest,
}; };
use server::indexer::IndexCommand; use server::indexer::IndexCommand;
use server::table_definition::handlers::post_table_definition; use server::table_definition::handlers::post_table_definition;

View File

@@ -8,7 +8,6 @@ use server::table_definition::handlers::post_table_definition;
use server::tables_data::handlers::get_table_data_by_position; use server::tables_data::handlers::get_table_data_by_position;
use crate::common::setup_test_db; use crate::common::setup_test_db;
use chrono::Utc; use chrono::Utc;
use serde_json::json;
#[fixture] #[fixture]
async fn pool() -> PgPool { async fn pool() -> PgPool {

View File

@@ -5,14 +5,12 @@ use common::proto::komp_ac::tables_data::GetTableDataRequest;
use crate::common::setup_test_db; use crate::common::setup_test_db;
use sqlx::{PgPool, Row}; use sqlx::{PgPool, Row};
use tonic; use tonic;
use chrono::{DateTime, Utc}; use chrono::Utc;
use serde_json::json; use serde_json::json;
use std::collections::HashMap; use std::collections::HashMap;
use futures::future::join_all; use futures::future::join_all;
use rand::distr::Alphanumeric; use rand::distr::Alphanumeric;
use rand::Rng; use rand::Rng;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use server::table_definition::handlers::post_table_definition; use server::table_definition::handlers::post_table_definition;
use server::tables_data::handlers::post_table_data; use server::tables_data::handlers::post_table_data;
use common::proto::komp_ac::table_definition::{ use common::proto::komp_ac::table_definition::{
@@ -22,7 +20,6 @@ use common::proto::komp_ac::tables_data::PostTableDataRequest;
use prost_types::Value; use prost_types::Value;
use prost_types::value::Kind; use prost_types::value::Kind;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use server::indexer::IndexCommand;
#[fixture] #[fixture]
async fn pool() -> PgPool { async fn pool() -> PgPool {
@@ -67,10 +64,10 @@ async fn table_definition(#[future] schema: (PgPool, String, i64)) -> (PgPool, S
// Define columns and indexes for the table // Define columns and indexes for the table
let columns = json!([ let columns = json!([
"\"name\" TEXT", { "name": "name", "field_type": "text" },
"\"age\" INTEGER", { "name": "age", "field_type": "integer" },
"\"email\" TEXT", { "name": "email", "field_type": "text" },
"\"is_active\" BOOLEAN" { "name": "is_active", "field_type": "boolean" }
]); ]);
let indexes = json!([]); let indexes = json!([]);

View File

@@ -795,7 +795,7 @@ async fn test_retrieve_from_nonexistent_schema() {
#[rstest] #[rstest]
#[tokio::test] #[tokio::test]
async fn test_retrieve_with_database_connection_error() { async fn test_retrieve_with_database_connection_error() {
let mut closed_pool = setup_test_db().await; let closed_pool = setup_test_db().await;
closed_pool.close().await; closed_pool.close().await;
let request = GetTableDataRequest { let request = GetTableDataRequest {

View File

@@ -20,7 +20,6 @@ use server::indexer::IndexCommand;
use sqlx::Row; use sqlx::Row;
use rand::distr::Alphanumeric; use rand::distr::Alphanumeric;
use rand::Rng; use rand::Rng;
use rust_decimal::prelude::FromPrimitive;
// Helper function to generate unique identifiers for test isolation // Helper function to generate unique identifiers for test isolation
fn generate_unique_id() -> String { fn generate_unique_id() -> String {
@@ -154,7 +153,7 @@ async fn test_context() -> TestContext {
#[fixture] #[fixture]
async fn closed_test_context() -> TestContext { async fn closed_test_context() -> TestContext {
let mut context = test_context().await; let context = test_context().await;
context.pool.close().await; context.pool.close().await;
context context
} }

View File

@@ -91,7 +91,7 @@ async fn create_initial_record(
// Set different initial values based on the test case to satisfy validation scripts // Set different initial values based on the test case to satisfy validation scripts
match (profile_name, table_name) { match (profile_name, table_name) {
("test_put_complex", "order") => { ("test_put_complex", "order") => {
// For complex formula: (+ (* @price @quantity) (* (* @price @quantity) 0.08)) // For complex formula: (+ (* $price $quantity) (* (* $price $quantity) 0.08))
// With price=10.00, quantity=1: (10*1) + (10*1*0.08) = 10 + 0.8 = 10.8 // With price=10.00, quantity=1: (10*1) + (10*1*0.08) = 10 + 0.8 = 10.8
data.insert("price".to_string(), ProtoValue { kind: Some(Kind::StringValue("10.00".to_string())) }); data.insert("price".to_string(), ProtoValue { kind: Some(Kind::StringValue("10.00".to_string())) });
data.insert("quantity".to_string(), ProtoValue { kind: Some(Kind::NumberValue(1.0)) }); data.insert("quantity".to_string(), ProtoValue { kind: Some(Kind::NumberValue(1.0)) });
@@ -99,7 +99,7 @@ async fn create_initial_record(
data.insert("percentage".to_string(), ProtoValue { kind: Some(Kind::StringValue("100.00".to_string())) }); data.insert("percentage".to_string(), ProtoValue { kind: Some(Kind::StringValue("100.00".to_string())) });
}, },
("test_put_division", "calculation") => { ("test_put_division", "calculation") => {
// For division: (/ @total @price) // For division: (/ $total $price)
// With total=10.00, price=10.00: 10/10 = 1 // With total=10.00, price=10.00: 10/10 = 1
data.insert("price".to_string(), ProtoValue { kind: Some(Kind::StringValue("10.00".to_string())) }); data.insert("price".to_string(), ProtoValue { kind: Some(Kind::StringValue("10.00".to_string())) });
data.insert("quantity".to_string(), ProtoValue { kind: Some(Kind::NumberValue(1.0)) }); data.insert("quantity".to_string(), ProtoValue { kind: Some(Kind::NumberValue(1.0)) });
@@ -142,7 +142,7 @@ async fn test_put_basic_arithmetic_validation_success(pool: PgPool) {
let script_request = PostTableScriptRequest { let script_request = PostTableScriptRequest {
table_definition_id: table_def_id, table_definition_id: table_def_id,
target_column: "total".to_string(), target_column: "total".to_string(),
script: "(* @price @quantity)".to_string(), script: "(* $price $quantity)".to_string(),
description: "Total = Price × Quantity".to_string(), description: "Total = Price × Quantity".to_string(),
}; };
post_table_script(&pool, script_request).await.unwrap(); post_table_script(&pool, script_request).await.unwrap();
@@ -180,7 +180,7 @@ async fn test_put_basic_arithmetic_validation_failure(pool: PgPool) {
let script_request = PostTableScriptRequest { let script_request = PostTableScriptRequest {
table_definition_id: table_def_id, table_definition_id: table_def_id,
target_column: "total".to_string(), target_column: "total".to_string(),
script: "(* @price @quantity)".to_string(), script: "(* $price $quantity)".to_string(),
description: "Total = Price × Quantity".to_string(), description: "Total = Price × Quantity".to_string(),
}; };
post_table_script(&pool, script_request).await.unwrap(); post_table_script(&pool, script_request).await.unwrap();
@@ -224,7 +224,7 @@ async fn test_put_complex_formula_validation(pool: PgPool) {
let script_request = PostTableScriptRequest { let script_request = PostTableScriptRequest {
table_definition_id: table_def_id, table_definition_id: table_def_id,
target_column: "total".to_string(), target_column: "total".to_string(),
script: "(+ (* @price @quantity) (* (* @price @quantity) 0.08))".to_string(), script: "(+ (* $price $quantity) (* (* $price $quantity) 0.08))".to_string(),
description: "Total with 8% tax".to_string(), description: "Total with 8% tax".to_string(),
}; };
post_table_script(&pool, script_request).await.unwrap(); post_table_script(&pool, script_request).await.unwrap();
@@ -261,7 +261,7 @@ async fn test_put_division_with_precision(pool: PgPool) {
let script_request = PostTableScriptRequest { let script_request = PostTableScriptRequest {
table_definition_id: table_def_id, table_definition_id: table_def_id,
target_column: "percentage".to_string(), target_column: "percentage".to_string(),
script: "(/ @total @price)".to_string(), script: "(/ $total $price)".to_string(),
description: "Percentage = Total / Price".to_string(), description: "Percentage = Total / Price".to_string(),
}; };
post_table_script(&pool, script_request).await.unwrap(); post_table_script(&pool, script_request).await.unwrap();
@@ -326,7 +326,7 @@ async fn test_put_advanced_math_functions(pool: PgPool) {
let sqrt_script = PostTableScriptRequest { let sqrt_script = PostTableScriptRequest {
table_definition_id: table_row.id, table_definition_id: table_row.id,
target_column: "square_root".to_string(), target_column: "square_root".to_string(),
script: "(sqrt @input)".to_string(), script: "(sqrt $input)".to_string(),
description: "Square root validation".to_string(), description: "Square root validation".to_string(),
}; };
post_table_script(&pool, sqrt_script).await.unwrap(); post_table_script(&pool, sqrt_script).await.unwrap();
@@ -334,7 +334,7 @@ async fn test_put_advanced_math_functions(pool: PgPool) {
let power_script = PostTableScriptRequest { let power_script = PostTableScriptRequest {
table_definition_id: table_row.id, table_definition_id: table_row.id,
target_column: "power_result".to_string(), target_column: "power_result".to_string(),
script: "(^ @input 2.0)".to_string(), script: "(^ $input 2.0)".to_string(),
description: "Power function validation".to_string(), description: "Power function validation".to_string(),
}; };
post_table_script(&pool, power_script).await.unwrap(); post_table_script(&pool, power_script).await.unwrap();
@@ -389,7 +389,7 @@ async fn test_put_financial_calculations(pool: PgPool) {
let compound_script = PostTableScriptRequest { let compound_script = PostTableScriptRequest {
table_definition_id: table_row.id, table_definition_id: table_row.id,
target_column: "compound_result".to_string(), target_column: "compound_result".to_string(),
script: "(* @principal (^ (+ 1.0 @rate) @time))".to_string(), script: "(* $principal (^ (+ 1.0 $rate) $time))".to_string(),
description: "Compound interest calculation".to_string(), description: "Compound interest calculation".to_string(),
}; };
post_table_script(&pool, compound_script).await.unwrap(); post_table_script(&pool, compound_script).await.unwrap();
@@ -397,7 +397,7 @@ async fn test_put_financial_calculations(pool: PgPool) {
let percentage_script = PostTableScriptRequest { let percentage_script = PostTableScriptRequest {
table_definition_id: table_row.id, table_definition_id: table_row.id,
target_column: "percentage_result".to_string(), target_column: "percentage_result".to_string(),
script: "(* @principal @rate)".to_string(), script: "(* $principal $rate)".to_string(),
description: "Percentage calculation".to_string(), description: "Percentage calculation".to_string(),
}; };
post_table_script(&pool, percentage_script).await.unwrap(); post_table_script(&pool, percentage_script).await.unwrap();
@@ -441,15 +441,13 @@ async fn test_put_partial_update_with_validation(pool: PgPool) {
let script_request = PostTableScriptRequest { let script_request = PostTableScriptRequest {
table_definition_id: table_def_id, table_definition_id: table_def_id,
target_column: "total".to_string(), target_column: "total".to_string(),
script: "(* @price @quantity)".to_string(), script: r#"( * (get-var "price") (get-var "quantity") )"#.to_string(),
description: "Total = Price × Quantity".to_string(), description: "Total = Price × Quantity".to_string(),
}; };
post_table_script(&pool, script_request).await.unwrap(); post_table_script(&pool, script_request).await.unwrap();
let record_id = create_initial_record(&pool, "test_put_partial", "invoice", &indexer_tx).await; let record_id = create_initial_record(&pool, "test_put_partial", "invoice", &indexer_tx).await;
// Partial update: only update quantity. The script detects this would change total
// from 10.00 to 50.00 and requires the user to include 'total' in the update.
let mut update_data = HashMap::new(); let mut update_data = HashMap::new();
update_data.insert("quantity".to_string(), ProtoValue { update_data.insert("quantity".to_string(), ProtoValue {
kind: Some(Kind::NumberValue(5.0)), kind: Some(Kind::NumberValue(5.0)),
@@ -462,16 +460,16 @@ async fn test_put_partial_update_with_validation(pool: PgPool) {
data: update_data, data: update_data,
}; };
// This should fail because script would change total value // This should fail because script would change total value (Case B: implicit change detection)
let result = put_table_data(&pool, put_request, &indexer_tx).await; let result = put_table_data(&pool, put_request, &indexer_tx).await;
assert!(result.is_err()); assert!(result.is_err());
let error = result.unwrap_err(); let error = result.unwrap_err();
assert_eq!(error.code(), tonic::Code::FailedPrecondition); assert_eq!(error.code(), tonic::Code::FailedPrecondition);
assert!(error.message().contains("Script for column 'total' was triggered")); let msg = error.message();
assert!(error.message().contains("from '10.00' to '50.00'")); assert!(msg.contains("Script for column 'total' was triggered"));
assert!(msg.contains("from '10.00' to '50.00'"));
assert!(msg.contains("include 'total' in your update request")); // Full change detection msg
// Now, test a partial update that SHOULD fail validation.
// We update quantity and provide an incorrect total.
let mut failing_update_data = HashMap::new(); let mut failing_update_data = HashMap::new();
failing_update_data.insert("quantity".to_string(), ProtoValue { failing_update_data.insert("quantity".to_string(), ProtoValue {
kind: Some(Kind::NumberValue(3.0)), kind: Some(Kind::NumberValue(3.0)),
@@ -491,8 +489,9 @@ async fn test_put_partial_update_with_validation(pool: PgPool) {
assert!(result.is_err()); assert!(result.is_err());
let error = result.unwrap_err(); let error = result.unwrap_err();
assert_eq!(error.code(), tonic::Code::InvalidArgument); assert_eq!(error.code(), tonic::Code::InvalidArgument);
assert!(error.message().contains("Script calculated '30.00'")); let msg = error.message();
assert!(error.message().contains("but user provided '99.99'")); assert!(msg.contains("Script calculated '30.00'"));
assert!(msg.contains("but user provided '99.99'"));
} }
#[sqlx::test] #[sqlx::test]
@@ -553,7 +552,7 @@ async fn test_put_steel_script_error_handling(pool: PgPool) {
let script_request = PostTableScriptRequest { let script_request = PostTableScriptRequest {
table_definition_id: table_def_id, table_definition_id: table_def_id,
target_column: "total".to_string(), target_column: "total".to_string(),
script: "(/ @price 0.0)".to_string(), script: "(/ $price 0.0)".to_string(),
description: "Error test".to_string(), description: "Error test".to_string(),
}; };
post_table_script(&pool, script_request).await.unwrap(); post_table_script(&pool, script_request).await.unwrap();
@@ -623,7 +622,7 @@ async fn test_decimal_precision_behavior(pool: PgPool) {
let script_request = PostTableScriptRequest { let script_request = PostTableScriptRequest {
table_definition_id: table_row.id, table_definition_id: table_row.id,
target_column: "result".to_string(), target_column: "result".to_string(),
script: "(/ @dividend @divisor)".to_string(), script: "(/ $dividend $divisor)".to_string(),
description: "Division test for precision".to_string(), description: "Division test for precision".to_string(),
}; };
post_table_script(&pool, script_request).await.unwrap(); post_table_script(&pool, script_request).await.unwrap();
@@ -816,7 +815,7 @@ async fn test_put_complex_formula_validation_via_handlers(pool: PgPool) {
"test_put_complex_handlers", "test_put_complex_handlers",
"order", "order",
"total", "total",
"(+ (* @price @quantity) (* (* @price @quantity) 0.08))", // Total with 8% tax "(+ (* $price $quantity) (* (* $price $quantity) 0.08))", // Total with 8% tax
) )
.await .await
.expect("Failed to add validation script"); .expect("Failed to add validation script");
@@ -891,7 +890,7 @@ async fn test_put_basic_arithmetic_validation_via_handlers(pool: PgPool) {
"test_put_arithmetic_handlers", "test_put_arithmetic_handlers",
"invoice", "invoice",
"total", "total",
"(* @price @quantity)", // Simple: Total = Price × Quantity "(* $price $quantity)", // Simple: Total = Price × Quantity
) )
.await .await
.expect("Failed to add validation script"); .expect("Failed to add validation script");
@@ -955,7 +954,7 @@ async fn test_put_arithmetic_validation_failure_via_handlers(pool: PgPool) {
"test_put_arithmetic_fail_handlers", "test_put_arithmetic_fail_handlers",
"invoice", "invoice",
"total", "total",
"(* @price @quantity)", "(* $price $quantity)",
) )
.await .await
.expect("Failed to add validation script"); .expect("Failed to add validation script");

View File

@@ -5,7 +5,7 @@
// ======================================================================== // ========================================================================
// Additional imports needed for these tests // Additional imports needed for these tests
use chrono::{DateTime, Utc}; use chrono::Utc;
use rust_decimal::Decimal; use rust_decimal::Decimal;
use std::str::FromStr; use std::str::FromStr;

View File

@@ -8,7 +8,7 @@
// This is needed for the database error test. // This is needed for the database error test.
#[fixture] #[fixture]
async fn closed_test_context() -> TestContext { async fn closed_test_context() -> TestContext {
let mut context = test_context().await; let context = test_context().await;
context.pool.close().await; context.pool.close().await;
context context
} }