Compare commits

...

52 Commits

Author SHA1 Message Date
filipriec
8127c7bb1b renamed again and fixed some minor stuff 2025-07-25 18:18:00 +02:00
filipriec
7437908baf removal of @ syntax as reference from the backend 2025-07-25 12:13:51 +02:00
filipriec
9eb46cb5d3 kompAC 2025-07-25 11:59:18 +02:00
filipriec
38a70128b0 prod code 2, time for new functionality 2025-07-25 00:08:16 +02:00
filipriec
c58ce52b33 fixing warnings and making prod code 2025-07-24 23:57:21 +02:00
filipriec
c82813185f code cleanup in post endpoint 2025-07-24 22:18:36 +02:00
filipriec
a96681e9d6 cleaned up code 2025-07-24 22:04:55 +02:00
filipriec
4df6c40034 removal of hardcoded fix, now working in general 2025-07-24 21:56:07 +02:00
filipriec
089d728cc7 passers 2025-07-24 10:55:03 +02:00
filipriec
aca3d718b5 CHECK THIS COMMIT I HAVE NO CLUE IF ITS CORRECT 2025-07-23 23:30:56 +02:00
filipriec
8a6a584cf3 compiletime error in test fixed 2025-07-23 00:43:11 +02:00
filipriec
00ed0cf796 test is now passing 2025-07-22 18:31:26 +02:00
filipriec
7e54b2fe43 we got a full passer now 2025-07-20 14:58:39 +02:00
filipriec
84871faad4 another passer, 2 more to go 2025-07-20 11:57:46 +02:00
filipriec
bcb433d7b2 fixing post table script 2025-07-20 11:46:00 +02:00
filipriec
7d1b130b68 we have a passer 2025-07-17 22:55:54 +02:00
filipriec
24c2376ea1 crucial self reference allowed 2025-07-17 22:06:53 +02:00
filipriec
810ef5fc10 post table script is now aware of a type in the database 2025-07-17 21:16:49 +02:00
filipriec
fe246b1fe6 reject boolean and text in math functions at the post table script 2025-07-16 22:31:55 +02:00
filipriec
de42bb48aa deprecated function removed, no need for backup 2025-07-13 08:51:47 +02:00
filipriec
17495c49ac steel scripts now have far better logic than before 2025-07-12 23:06:21 +02:00
filipriec
0e3a7a06a3 put needs more adjustements 2025-07-12 11:06:11 +02:00
filipriec
e0ee48eb9c put endpoint is now stronq boiii 2025-07-11 23:24:28 +02:00
filipriec
d2053b1d5a SCRIPTS only scripts reference to a linked table from this commit 2025-07-11 08:55:32 +02:00
filipriec
fbe8e53858 circular dependency fix in post script logic 2025-07-11 08:50:10 +02:00
filipriec
8fe2581b3f put request at halt until script fixes 2025-07-10 21:11:42 +02:00
filipriec
60cc0e562e adjusted tests for the put request 2025-07-09 22:24:33 +02:00
filipriec
26898d474f tests for steel in post request is fixed with all the errors found in the codebase 2025-07-08 22:04:11 +02:00
filipriec
2311fbaa3b post steel tests 2025-07-08 20:26:48 +02:00
filipriec
be99cd9423 forgotten lock 2025-07-08 18:07:28 +02:00
filipriec
a3dd6fa95b now fully functional without a steel decimal crate, we are only importing it via cargo, because steel decimal is a separate published crate now 2025-07-08 18:02:32 +02:00
filipriec
433d87c96d stuff around publishing crate successfuly done 2025-07-07 22:08:38 +02:00
filipriec
aff4383671 tests are passing well now 2025-07-07 20:29:51 +02:00
filipriec
b7c8f6b1a2 tests in the steel decimal crate with serious issue fixed 2025-07-07 19:24:08 +02:00
filipriec
3443839ba4 placing them properly 2025-07-07 18:47:50 +02:00
filipriec
6c31d48f3b steel decimal needs readme before being published to the crates io 2025-07-07 18:08:45 +02:00
filipriec
1770292fd8 all the tests are now passing perfectly well 2025-07-07 15:35:33 +02:00
filipriec
afdd5c5740 parser finally fixed 2025-07-07 15:07:08 +02:00
filipriec
11487f0833 more fixes, still not done yet 2025-07-07 13:32:06 +02:00
filipriec
4d5d22d0c2 precision to steel decimal crate implemented 2025-07-07 00:31:13 +02:00
filipriec
314a957922 fixes, now .0 from rust decimal is being discussed 2025-07-06 20:53:40 +02:00
filipriec
4c57b562e6 more fixes, not there yet tho 2025-07-05 10:00:04 +02:00
filipriec
a757acf51c we are now fully using steel decimal crate inside of the server crate 2025-07-04 13:11:47 +02:00
filipriec
f4a23be1a2 steel decimal crate with a proper setup, needs so much work to be done to be finished 2025-07-04 11:56:21 +02:00
filipriec
93c67ffa14 steel decimal crate implemented 2025-07-02 16:31:15 +02:00
filipriec
d1ebe4732f steel with decimal math, saving before separating steel to a separate crate 2025-07-02 14:44:37 +02:00
filipriec
7b7f3ca05a more tests for the frontend 2025-06-26 20:25:59 +02:00
filipriec
234613f831 more frontend tests 2025-06-26 20:03:47 +02:00
filipriec
f6d84e70cc testing frontend to connect to the backend in the form page 2025-06-26 19:19:08 +02:00
filipriec
5cd324b6ae client tests now have a proper structure 2025-06-26 11:56:18 +02:00
filipriec
a7457f5749 frontend tui tests 2025-06-25 23:00:51 +02:00
filipriec
a5afc75099 crit bug fixed 2025-06-25 17:33:37 +02:00
130 changed files with 13895 additions and 1128 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@
.env
/tantivy_indexes
server/tantivy_indexes
steel_decimal/tests/property_tests.proptest-regressions

933
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,14 +4,14 @@ resolver = "2"
[workspace.package]
# TODO: idk how to do the name, fix later
# name = "Multieko2"
# name = "komp_ac"
version = "0.3.13"
edition = "2021"
license = "GPL-3.0-or-later"
authors = ["Filip Priečinský <filippriec@gmail.com>"]
description = "Poriadny uctovnicky software."
readme = "README.md"
repository = "https://gitlab.com/filipriec/multieko2"
repository = "https://gitlab.com/filipriec/komp_ac"
categories = ["command-line-interface"]
# [workspace.metadata]
@@ -40,4 +40,10 @@ tracing = "0.1.41"
# Search crate
tantivy = "0.24.1"
# Steel_decimal crate
rust_decimal = { version = "1.37.2", features = ["maths", "serde"] }
rust_decimal_macros = "1.37.1"
thiserror = "2.0.12"
regex = "1.11.1"
common = { path = "./common" }

View File

@@ -31,3 +31,9 @@ unicode-width = "0.2.0"
[features]
default = []
ui-debug = []
[dev-dependencies]
rstest = "0.25.0"
tokio-test = "0.4.4"
uuid = { version = "1.17.0", features = ["v4"] }
futures = "0.3.31"

View File

@@ -4,7 +4,7 @@ use crate::config::colors::themes::Theme;
use crate::state::pages::auth::AuthState;
use crate::state::app::state::AppState;
use crate::state::pages::admin::AdminState;
use common::proto::multieko2::table_definition::ProfileTreeResponse;
use common::proto::komp_ac::table_definition::ProfileTreeResponse;
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::Style,

View File

@@ -2,7 +2,7 @@
use crate::config::colors::themes::Theme;
use crate::state::pages::form::FormState;
use common::proto::multieko2::search::search_response::Hit;
use common::proto::komp_ac::search::search_response::Hit;
use ratatui::{
layout::Rect,
style::{Color, Modifier, Style},

View File

@@ -47,7 +47,7 @@ pub fn render_status_line(
}
// --- The normal status line rendering logic (unchanged) ---
let program_info = format!("multieko2 v{}", env!("CARGO_PKG_VERSION"));
let program_info = format!("komp_ac v{}", env!("CARGO_PKG_VERSION"));
let mode_text = if is_edit_mode { "[EDIT]" } else { "[READ-ONLY]" };
let home_dir = dirs::home_dir()

View File

@@ -1,10 +1,10 @@
// src/components/form/form.rs
use crate::components::common::autocomplete; // <--- ADD THIS IMPORT
use crate::components::common::autocomplete;
use crate::components::handlers::canvas::render_canvas;
use crate::config::colors::themes::Theme;
use crate::state::app::highlight::HighlightState;
use crate::state::pages::canvas_state::CanvasState;
use crate::state::pages::form::FormState; // <--- CHANGE THIS IMPORT
use crate::state::pages::form::FormState;
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
style::Style,

View File

@@ -6,7 +6,7 @@ use ratatui::{
Frame,
};
use crate::config::colors::themes::Theme;
use common::proto::multieko2::table_definition::{ProfileTreeResponse};
use common::proto::komp_ac::table_definition::{ProfileTreeResponse};
use ratatui::text::{Span, Line};
use crate::components::utils::text::truncate_string;

View File

@@ -32,7 +32,7 @@ pub fn render_intro(f: &mut Frame, intro_state: &IntroState, area: Rect, theme:
// Title
let title = Line::from(vec![
Span::styled("multieko2", Style::default().fg(theme.highlight)),
Span::styled("komp_ac", Style::default().fg(theme.highlight)),
Span::styled(" v", Style::default().fg(theme.fg)),
Span::styled(env!("CARGO_PKG_VERSION"), Style::default().fg(theme.secondary)),
]);

View File

@@ -9,7 +9,7 @@ use tracing::{error, info};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
pub const APP_NAME: &str = "multieko2_client";
pub const APP_NAME: &str = "komp_ac_client";
pub const TOKEN_FILE_NAME: &str = "auth.token";
#[derive(Serialize, Deserialize, Debug, Clone)]

View File

@@ -13,7 +13,7 @@ use crate::state::pages::{
form::FormState,
};
use anyhow::Result;
use common::proto::multieko2::search::search_response::Hit;
use common::proto::komp_ac::search::search_response::Hit;
use crossterm::event::{KeyCode, KeyEvent};
use tokio::sync::mpsc;
use tracing::{debug, info};

View File

@@ -2,7 +2,7 @@
use crate::config::binds::config::Config;
use crate::modes::handlers::event::EventOutcome;
use anyhow::Result;
use common::proto::multieko2::table_definition::ProfileTreeResponse;
use common::proto::komp_ac::table_definition::ProfileTreeResponse;
use crossterm::event::{KeyCode, KeyEvent};
use std::collections::{HashMap, HashSet};

View File

@@ -42,7 +42,7 @@ use crate::tui::{
use crate::ui::handlers::context::UiContext;
use crate::ui::handlers::rat_state::UiStateHandler;
use anyhow::Result;
use common::proto::multieko2::search::search_response::Hit;
use common::proto::komp_ac::search::search_response::Hit;
use crossterm::cursor::SetCursorStyle;
use crossterm::event::{Event, KeyCode, KeyEvent};
use tokio::sync::mpsc;

View File

@@ -1,6 +1,6 @@
// src/services/auth.rs
use tonic::transport::Channel;
use common::proto::multieko2::auth::{
use common::proto::komp_ac::auth::{
auth_service_client::AuthServiceClient,
LoginRequest, LoginResponse,
RegisterRequest, AuthResponse,

View File

@@ -1,25 +1,28 @@
// src/services/grpc_client.rs
use common::proto::multieko2::common::Empty;
use common::proto::multieko2::table_structure::table_structure_service_client::TableStructureServiceClient;
use common::proto::multieko2::table_structure::{GetTableStructureRequest, TableStructureResponse};
use common::proto::multieko2::table_definition::{
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::{GetTableStructureRequest, TableStructureResponse};
use common::proto::komp_ac::table_definition::{
table_definition_client::TableDefinitionClient,
PostTableDefinitionRequest, ProfileTreeResponse, TableDefinitionResponse,
};
use common::proto::multieko2::table_script::{
use common::proto::komp_ac::table_script::{
table_script_client::TableScriptClient,
PostTableScriptRequest, TableScriptResponse,
};
use common::proto::multieko2::tables_data::{
use common::proto::komp_ac::tables_data::{
tables_data_client::TablesDataClient,
GetTableDataByPositionRequest,
GetTableDataRequest, // ADD THIS
GetTableDataResponse,
DeleteTableDataRequest, // ADD THIS
DeleteTableDataResponse, // ADD THIS
GetTableDataCountRequest,
PostTableDataRequest, PostTableDataResponse, PutTableDataRequest,
PutTableDataResponse,
};
use common::proto::multieko2::search::{
use common::proto::komp_ac::search::{
searcher_client::SearcherClient, SearchRequest, SearchResponse,
};
use anyhow::{Context, Result};
@@ -116,7 +119,7 @@ impl GrpcClient {
Ok(response.into_inner())
}
// NEW Methods for TablesData service
// Existing TablesData methods
pub async fn get_table_data_count(
&mut self,
profile_name: String,
@@ -135,7 +138,7 @@ impl GrpcClient {
Ok(response.into_inner().count as u64)
}
pub async fn get_table_data_by_position(
pub async fn get_table_data_by_position(
&mut self,
profile_name: String,
table_name: String,
@@ -155,18 +158,58 @@ pub async fn get_table_data_by_position(
Ok(response.into_inner())
}
// ADD THIS: Missing get_table_data method
pub async fn get_table_data(
&mut self,
profile_name: String,
table_name: String,
id: i64,
) -> Result<GetTableDataResponse> {
let grpc_request = GetTableDataRequest {
profile_name,
table_name,
id,
};
let request = tonic::Request::new(grpc_request);
let response = self
.tables_data_client
.get_table_data(request)
.await
.context("gRPC GetTableData call failed")?;
Ok(response.into_inner())
}
// ADD THIS: Missing delete_table_data method
pub async fn delete_table_data(
&mut self,
profile_name: String,
table_name: String,
record_id: i64,
) -> Result<DeleteTableDataResponse> {
let grpc_request = DeleteTableDataRequest {
profile_name,
table_name,
record_id,
};
let request = tonic::Request::new(grpc_request);
let response = self
.tables_data_client
.delete_table_data(request)
.await
.context("gRPC DeleteTableData call failed")?;
Ok(response.into_inner())
}
pub async fn post_table_data(
&mut self,
profile_name: String,
table_name: String,
// CHANGE THIS: Accept the pre-converted data
data: HashMap<String, Value>,
) -> Result<PostTableDataResponse> {
// The conversion logic is now gone from here.
let grpc_request = PostTableDataRequest {
profile_name,
table_name,
data, // This is now the correct type
data,
};
let request = tonic::Request::new(grpc_request);
let response = self
@@ -182,15 +225,13 @@ pub async fn get_table_data_by_position(
profile_name: String,
table_name: String,
id: i64,
// CHANGE THIS: Accept the pre-converted data
data: HashMap<String, Value>,
) -> Result<PutTableDataResponse> {
// The conversion logic is now gone from here.
let grpc_request = PutTableDataRequest {
profile_name,
table_name,
id,
data, // This is now the correct type
data,
};
let request = tonic::Request::new(grpc_request);
let response = self

View File

@@ -98,7 +98,7 @@ impl UiService {
pub async fn initialize_add_logic_table_data(
grpc_client: &mut GrpcClient,
add_logic_state: &mut AddLogicState,
profile_tree: &common::proto::multieko2::table_definition::ProfileTreeResponse,
profile_tree: &common::proto::komp_ac::table_definition::ProfileTreeResponse,
) -> Result<String> {
let profile_name_clone_opt = Some(add_logic_state.profile_name.clone());
let table_name_opt_clone = add_logic_state.selected_table_name.clone();

View File

@@ -1,6 +1,6 @@
// src/state/app/search.rs
use common::proto::multieko2::search::search_response::Hit;
use common::proto::komp_ac::search::search_response::Hit;
/// Holds the complete state for the search palette.
pub struct SearchState {

View File

@@ -1,9 +1,9 @@
// src/state/app/state.rs
use anyhow::Result;
use common::proto::multieko2::table_definition::ProfileTreeResponse;
use common::proto::komp_ac::table_definition::ProfileTreeResponse;
// NEW: Import the types we need for the cache
use common::proto::multieko2::table_structure::TableStructureResponse;
use common::proto::komp_ac::table_structure::TableStructureResponse;
use crate::modes::handlers::mode_manager::AppMode;
use crate::state::app::search::SearchState;
use crate::ui::handlers::context::DialogPurpose;

View File

@@ -1,6 +1,6 @@
// src/state/pages/canvas_state.rs
use common::proto::multieko2::search::search_response::Hit;
use common::proto::komp_ac::search::search_response::Hit;
pub trait CanvasState {
// --- Existing methods (unchanged) ---

View File

@@ -3,7 +3,7 @@
use crate::config::colors::themes::Theme;
use crate::state::app::highlight::HighlightState;
use crate::state::pages::canvas_state::CanvasState;
use common::proto::multieko2::search::search_response::Hit;
use common::proto::komp_ac::search::search_response::Hit;
use ratatui::layout::Rect;
use ratatui::Frame;
use std::collections::HashMap;

View File

@@ -4,7 +4,7 @@ use crate::state::pages::add_table::{
};
use crate::services::GrpcClient;
use anyhow::{anyhow, Result};
use common::proto::multieko2::table_definition::{
use common::proto::komp_ac::table_definition::{
PostTableDefinitionRequest,
ColumnDefinition as ProtoColumnDefinition,
TableLink as ProtoTableLink,

View File

@@ -8,7 +8,7 @@ use crate::state::app::buffer::{AppView, BufferState};
use crate::config::storage::storage::{StoredAuthData, save_auth_data};
use crate::state::pages::canvas_state::CanvasState;
use crate::ui::handlers::context::DialogPurpose;
use common::proto::multieko2::auth::LoginResponse;
use common::proto::komp_ac::auth::LoginResponse;
use anyhow::{Context, Result};
use tokio::spawn;
use tokio::sync::mpsc;

View File

@@ -8,7 +8,7 @@ use crate::state::{
};
use crate::ui::handlers::context::DialogPurpose;
use crate::state::app::buffer::{AppView, BufferState};
use common::proto::multieko2::auth::AuthResponse;
use common::proto::komp_ac::auth::AuthResponse;
use anyhow::Context;
use tokio::spawn;
use tokio::sync::mpsc;

View File

@@ -1,6 +1,6 @@
// src/utils/data_converter.rs
use common::proto::multieko2::table_structure::TableStructureResponse;
use common::proto::komp_ac::table_structure::TableStructureResponse;
use prost_types::{value::Kind, NullValue, Value};
use std::collections::HashMap;

View File

@@ -0,0 +1,262 @@
// client/tests/form_tests.rs
use rstest::{fixture, rstest};
use std::collections::HashMap;
use client::state::pages::form::{FormState, FieldDefinition};
use client::state::pages::canvas_state::CanvasState;
#[fixture]
fn test_form_state() -> FormState {
let fields = vec![
FieldDefinition {
display_name: "Company".to_string(),
data_key: "firma".to_string(),
is_link: false,
link_target_table: None,
},
FieldDefinition {
display_name: "Phone".to_string(),
data_key: "telefon".to_string(),
is_link: false,
link_target_table: None,
},
FieldDefinition {
display_name: "Email".to_string(),
data_key: "email".to_string(),
is_link: false,
link_target_table: None,
},
];
FormState::new("test_profile".to_string(), "test_table".to_string(), fields)
}
#[fixture]
fn test_form_data() -> HashMap<String, String> {
let mut data = HashMap::new();
data.insert("firma".to_string(), "Test Company".to_string());
data.insert("telefon".to_string(), "+421123456789".to_string());
data.insert("email".to_string(), "test@example.com".to_string());
data
}
#[rstest]
fn test_form_state_creation(test_form_state: FormState) {
assert_eq!(test_form_state.profile_name, "test_profile");
assert_eq!(test_form_state.table_name, "test_table");
assert_eq!(test_form_state.fields.len(), 3);
assert_eq!(test_form_state.current_field(), 0);
assert!(!test_form_state.has_unsaved_changes());
}
#[rstest]
fn test_form_field_navigation(mut test_form_state: FormState) {
// Test initial field
assert_eq!(test_form_state.current_field(), 0);
// Test navigation to next field
test_form_state.set_current_field(1);
assert_eq!(test_form_state.current_field(), 1);
// Test navigation to last field
test_form_state.set_current_field(2);
assert_eq!(test_form_state.current_field(), 2);
// Test invalid field (should not crash)
test_form_state.set_current_field(999);
assert_eq!(test_form_state.current_field(), 2); // Should stay at valid field
}
#[rstest]
fn test_form_data_entry(mut test_form_state: FormState) {
// Test entering data in first field
*test_form_state.get_current_input_mut() = "Test Company".to_string();
test_form_state.set_has_unsaved_changes(true);
assert_eq!(test_form_state.get_current_input(), "Test Company");
assert!(test_form_state.has_unsaved_changes());
}
#[rstest]
fn test_form_field_switching_with_data(mut test_form_state: FormState) {
// Enter data in first field
*test_form_state.get_current_input_mut() = "Company Name".to_string();
// Switch to second field
test_form_state.set_current_field(1);
*test_form_state.get_current_input_mut() = "+421123456789".to_string();
// Switch back to first field
test_form_state.set_current_field(0);
assert_eq!(test_form_state.get_current_input(), "Company Name");
// Switch to second field again
test_form_state.set_current_field(1);
assert_eq!(test_form_state.get_current_input(), "+421123456789");
}
#[rstest]
fn test_form_reset_functionality(mut test_form_state: FormState) {
// Add some data
test_form_state.set_current_field(0);
*test_form_state.get_current_input_mut() = "Test Company".to_string();
test_form_state.set_current_field(1);
*test_form_state.get_current_input_mut() = "+421123456789".to_string();
test_form_state.set_has_unsaved_changes(true);
test_form_state.id = 123;
test_form_state.current_position = 5;
// Reset the form
test_form_state.reset_to_empty();
// Verify reset
assert_eq!(test_form_state.id, 0);
assert!(!test_form_state.has_unsaved_changes());
assert_eq!(test_form_state.current_field(), 0);
// Check all fields are empty
for i in 0..test_form_state.fields.len() {
test_form_state.set_current_field(i);
assert!(test_form_state.get_current_input().is_empty());
}
}
#[rstest]
fn test_form_update_from_response(mut test_form_state: FormState, test_form_data: HashMap<String, String>) {
let position = 3;
// Update form with response data
test_form_state.update_from_response(&test_form_data, position);
// Verify data was loaded
assert_eq!(test_form_state.current_position, position);
assert!(!test_form_state.has_unsaved_changes());
assert_eq!(test_form_state.current_field(), 0);
// Check field values
test_form_state.set_current_field(0);
assert_eq!(test_form_state.get_current_input(), "Test Company");
test_form_state.set_current_field(1);
assert_eq!(test_form_state.get_current_input(), "+421123456789");
test_form_state.set_current_field(2);
assert_eq!(test_form_state.get_current_input(), "test@example.com");
}
#[rstest]
fn test_form_cursor_position(mut test_form_state: FormState) {
// Test initial cursor position
assert_eq!(test_form_state.current_cursor_pos(), 0);
// Add some text
*test_form_state.get_current_input_mut() = "Test Company".to_string();
// Test cursor positioning
test_form_state.set_current_cursor_pos(5);
assert_eq!(test_form_state.current_cursor_pos(), 5);
// Test cursor bounds
test_form_state.set_current_cursor_pos(999);
// Should be clamped to text length
assert!(test_form_state.current_cursor_pos() <= "Test Company".len());
}
#[rstest]
fn test_form_field_display_names(test_form_state: FormState) {
let field_names = test_form_state.fields();
assert_eq!(field_names.len(), 3);
assert_eq!(field_names[0], "Company");
assert_eq!(field_names[1], "Phone");
assert_eq!(field_names[2], "Email");
}
#[rstest]
fn test_form_inputs_vector(mut test_form_state: FormState) {
// Add data to fields
test_form_state.set_current_field(0);
*test_form_state.get_current_input_mut() = "Company A".to_string();
test_form_state.set_current_field(1);
*test_form_state.get_current_input_mut() = "123456789".to_string();
test_form_state.set_current_field(2);
*test_form_state.get_current_input_mut() = "test@test.com".to_string();
// Get inputs vector
let inputs = test_form_state.inputs();
assert_eq!(inputs.len(), 3);
assert_eq!(inputs[0], "Company A");
assert_eq!(inputs[1], "123456789");
assert_eq!(inputs[2], "test@test.com");
}
#[rstest]
fn test_form_position_management(mut test_form_state: FormState) {
// Test initial position
assert_eq!(test_form_state.current_position, 1);
assert_eq!(test_form_state.total_count, 0);
// Set some values
test_form_state.total_count = 10;
test_form_state.current_position = 5;
assert_eq!(test_form_state.current_position, 5);
assert_eq!(test_form_state.total_count, 10);
// Test reset affects position
test_form_state.reset_to_empty();
assert_eq!(test_form_state.current_position, 11); // total_count + 1
}
#[rstest]
fn test_form_autocomplete_state(mut test_form_state: FormState) {
// Test initial autocomplete state
assert!(!test_form_state.autocomplete_active);
assert!(test_form_state.autocomplete_suggestions.is_empty());
assert!(test_form_state.selected_suggestion_index.is_none());
// Test deactivating autocomplete
test_form_state.autocomplete_active = true;
test_form_state.deactivate_autocomplete();
assert!(!test_form_state.autocomplete_active);
assert!(test_form_state.autocomplete_suggestions.is_empty());
assert!(test_form_state.selected_suggestion_index.is_none());
assert!(!test_form_state.autocomplete_loading);
}
#[rstest]
fn test_form_empty_data_handling(mut test_form_state: FormState) {
let empty_data = HashMap::new();
// Update with empty data
test_form_state.update_from_response(&empty_data, 1);
// All fields should be empty
for i in 0..test_form_state.fields.len() {
test_form_state.set_current_field(i);
assert!(test_form_state.get_current_input().is_empty());
}
}
#[rstest]
fn test_form_partial_data_handling(mut test_form_state: FormState) {
let mut partial_data = HashMap::new();
partial_data.insert("firma".to_string(), "Partial Company".to_string());
// Intentionally missing telefon and email
test_form_state.update_from_response(&partial_data, 1);
// First field should have data
test_form_state.set_current_field(0);
assert_eq!(test_form_state.get_current_input(), "Partial Company");
// Other fields should be empty
test_form_state.set_current_field(1);
assert!(test_form_state.get_current_input().is_empty());
test_form_state.set_current_field(2);
assert!(test_form_state.get_current_input().is_empty());
}

View File

@@ -0,0 +1 @@
pub mod form_tests;

2
client/tests/form/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod gui;
pub mod requests;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,267 @@
// ========================================================================
// ROBUST WORKFLOW AND INTEGRATION TESTS
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_partial_update_preserves_other_fields(
#[future] populated_test_context: FormTestContext,
) {
let mut context = populated_test_context.await;
skip_if_backend_unavailable!();
// 1. Create a record with multiple fields
let mut initial_data = context.create_test_form_data();
let original_email = "preserve.this@email.com";
initial_data.insert(
"email".to_string(),
create_string_value(original_email),
);
let post_res = context
.client
.post_table_data(
context.profile_name.clone(),
context.table_name.clone(),
initial_data,
)
.await
.expect("Setup: Failed to create record for partial update test");
let created_id = post_res.inserted_id;
println!("Partial Update Test: Created record ID {}", created_id);
// 2. Update only ONE field
let mut partial_update = HashMap::new();
let updated_firma = "Partially Updated Inc.";
partial_update.insert(
"firma".to_string(),
create_string_value(updated_firma),
);
context
.client
.put_table_data(
context.profile_name.clone(),
context.table_name.clone(),
created_id,
partial_update,
)
.await
.expect("Partial update failed");
println!("Partial Update Test: Updated only 'firma' field");
// 3. Get the record back and verify ALL fields
let get_res = context
.client
.get_table_data(
context.profile_name.clone(),
context.table_name.clone(),
created_id,
)
.await
.expect("Failed to get record after partial update");
let final_data = get_res.data;
assert_eq!(
final_data.get("firma").unwrap(),
updated_firma,
"The 'firma' field should be updated"
);
assert_eq!(
final_data.get("email").unwrap(),
original_email,
"The 'email' field should have been preserved"
);
println!("Partial Update Test: Verified other fields were preserved. OK.");
}
#[rstest]
#[tokio::test]
async fn test_data_edge_cases_and_unicode(
#[future] form_test_context: FormTestContext,
) {
let mut context = form_test_context.await;
skip_if_backend_unavailable!();
let edge_case_strings = vec![
("Unicode", "José María González, Москва, 北京市"),
("Emoji", "🚀 Tech Company 🌟"),
("Quotes", "Quote\"Test'Apostrophe"),
("Symbols", "Price: $1,000.50 (50% off!)"),
("Empty", ""),
("Whitespace", " "),
];
for (case_name, test_string) in edge_case_strings {
let mut data = HashMap::new();
data.insert("firma".to_string(), create_string_value(test_string));
data.insert(
"kz".to_string(),
create_string_value(&format!("EDGE-{}", case_name)),
);
let post_res = context
.client
.post_table_data(
context.profile_name.clone(),
context.table_name.clone(),
data,
)
.await
.expect(&format!("POST should succeed for case: {}", case_name));
let created_id = post_res.inserted_id;
let get_res = context
.client
.get_table_data(
context.profile_name.clone(),
context.table_name.clone(),
created_id,
)
.await
.expect(&format!(
"GET should succeed for case: {}",
case_name
));
assert_eq!(
get_res.data.get("firma").unwrap(),
test_string,
"Data should be identical after round-trip for case: {}",
case_name
);
println!("Edge Case Test: '{}' passed.", case_name);
}
}
#[rstest]
#[tokio::test]
async fn test_numeric_and_null_edge_cases(
#[future] form_test_context: FormTestContext,
) {
let mut context = form_test_context.await;
skip_if_backend_unavailable!();
// 1. Test NULL value
let mut null_data = HashMap::new();
null_data.insert(
"firma".to_string(),
create_string_value("Company With Null Phone"),
);
null_data.insert("telefon".to_string(), create_null_value());
let post_res_null = context
.client
.post_table_data(
context.profile_name.clone(),
context.table_name.clone(),
null_data,
)
.await
.expect("POST with NULL value should succeed");
let get_res_null = context
.client
.get_table_data(
context.profile_name.clone(),
context.table_name.clone(),
post_res_null.inserted_id,
)
.await
.unwrap();
// Depending on DB, NULL may come back as empty string or be absent.
// The important part is that the operation doesn't fail.
assert!(
get_res_null.data.get("telefon").unwrap_or(&"".to_string()).is_empty(),
"NULL value should result in an empty or absent field"
);
println!("Edge Case Test: NULL value handled correctly. OK.");
// 2. Test Zero value for a numeric field (assuming 'age' is numeric)
let mut zero_data = HashMap::new();
zero_data.insert(
"firma".to_string(),
create_string_value("Newborn Company"),
);
// Assuming 'age' is a field in your actual table definition
// zero_data.insert("age".to_string(), create_number_value(0.0));
// let post_res_zero = context.client.post_table_data(...).await.expect("POST with zero should succeed");
// ... then get and verify it's "0"
println!("Edge Case Test: Zero value test skipped (uncomment if 'age' field exists).");
}
#[rstest]
#[tokio::test]
async fn test_concurrent_updates_on_same_record(
#[future] populated_test_context: FormTestContext,
) {
let mut context = populated_test_context.await;
skip_if_backend_unavailable!();
// 1. Create a single record to be updated by all tasks
let initial_data = context.create_minimal_form_data();
let post_res = context
.client
.post_table_data(
context.profile_name.clone(),
context.table_name.clone(),
initial_data,
)
.await
.expect("Setup: Failed to create record for concurrency test");
let record_id = post_res.inserted_id;
println!("Concurrency Test: Target record ID is {}", record_id);
// 2. Spawn multiple concurrent UPDATE operations
let mut handles = Vec::new();
let num_concurrent_tasks = 5;
let mut final_values = Vec::new();
for i in 0..num_concurrent_tasks {
let mut client_clone = context.client.clone();
let profile_name = context.profile_name.clone();
let table_name = context.table_name.clone();
let final_value = format!("Concurrent Update {}", i);
final_values.push(final_value.clone());
let handle = tokio::spawn(async move {
let mut update_data = HashMap::new();
update_data.insert(
"firma".to_string(),
create_string_value(&final_value),
);
client_clone
.put_table_data(profile_name, table_name, record_id, update_data)
.await
});
handles.push(handle);
}
// 3. Wait for all tasks to complete and check for panics
let results = futures::future::join_all(handles).await;
assert!(
results.iter().all(|r| r.is_ok()),
"No concurrent task should panic"
);
println!("Concurrency Test: All update tasks completed without panicking.");
// 4. Get the final state of the record
let final_get_res = context
.client
.get_table_data(
context.profile_name.clone(),
context.table_name.clone(),
record_id,
)
.await
.expect("Should be able to get the record after concurrent updates");
let final_firma = final_get_res.data.get("firma").unwrap();
assert!(
final_values.contains(final_firma),
"The final state '{}' must be one of the states set by the tasks",
final_firma
);
println!(
"Concurrency Test: Final state is '{}', which is a valid outcome. OK.",
final_firma
);
}

View File

@@ -0,0 +1,727 @@
// form_request_tests3.rs - Comprehensive and Robust Testing
// ========================================================================
// STEEL SCRIPT VALIDATION TESTS (HIGHEST PRIORITY)
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_steel_script_validation_success(#[future] form_test_context: FormTestContext) {
let mut context = form_test_context.await;
skip_if_backend_unavailable!();
// Test with data that should pass script validation
// Assuming there's a script that validates 'kz' field to start with "KZ" and be 5 chars
let mut valid_data = HashMap::new();
valid_data.insert("firma".to_string(), create_string_value("Script Test Company"));
valid_data.insert("kz".to_string(), create_string_value("KZ123"));
valid_data.insert("telefon".to_string(), create_string_value("+421123456789"));
let result = context.client.post_table_data(
context.profile_name.clone(),
context.table_name.clone(),
valid_data,
).await;
match result {
Ok(response) => {
assert!(response.success, "Valid data should pass script validation");
println!("Script Validation Test: Valid data passed - ID {}", response.inserted_id);
}
Err(e) => {
if let Some(status) = e.downcast_ref::<Status>() {
if status.code() == tonic::Code::Unavailable {
println!("Script validation test skipped - backend not available");
return;
}
// If there are no scripts configured, this might still work
println!("Script validation test: {}", status.message());
}
}
}
}
#[rstest]
#[tokio::test]
async fn test_steel_script_validation_failure(#[future] form_test_context: FormTestContext) {
let mut context = form_test_context.await;
skip_if_backend_unavailable!();
// Test with data that should fail script validation
let invalid_script_data = vec![
("TooShort", "KZ12"), // Too short
("TooLong", "KZ12345"), // Too long
("WrongPrefix", "AB123"), // Wrong prefix
("NoPrefix", "12345"), // No prefix
("Empty", ""), // Empty
];
for (test_case, invalid_kz) in invalid_script_data {
let mut invalid_data = HashMap::new();
invalid_data.insert("firma".to_string(), create_string_value("Script Fail Company"));
invalid_data.insert("kz".to_string(), create_string_value(invalid_kz));
let result = context.client.post_table_data(
context.profile_name.clone(),
context.table_name.clone(),
invalid_data,
).await;
match result {
Ok(_) => {
println!("Script Validation Test: {} passed (no validation script configured)", test_case);
}
Err(e) => {
if let Some(status) = e.downcast_ref::<Status>() {
assert_eq!(status.code(), tonic::Code::InvalidArgument,
"Script validation failure should return InvalidArgument for case: {}", test_case);
println!("Script Validation Test: {} correctly failed - {}", test_case, status.message());
}
}
}
}
}
#[rstest]
#[tokio::test]
async fn test_steel_script_validation_on_update(#[future] form_test_context: FormTestContext) {
let mut context = form_test_context.await;
skip_if_backend_unavailable!();
// 1. Create a valid record first
let mut initial_data = HashMap::new();
initial_data.insert("firma".to_string(), create_string_value("Update Script Test"));
initial_data.insert("kz".to_string(), create_string_value("KZ123"));
let post_result = context.client.post_table_data(
context.profile_name.clone(),
context.table_name.clone(),
initial_data,
).await;
if let Ok(post_response) = post_result {
let record_id = post_response.inserted_id;
// 2. Try to update with invalid data
let mut invalid_update = HashMap::new();
invalid_update.insert("kz".to_string(), create_string_value("INVALID"));
let update_result = context.client.put_table_data(
context.profile_name.clone(),
context.table_name.clone(),
record_id,
invalid_update,
).await;
match update_result {
Ok(_) => {
println!("Script Validation on Update: No validation script configured for updates");
}
Err(e) => {
if let Some(status) = e.downcast_ref::<Status>() {
assert_eq!(status.code(), tonic::Code::InvalidArgument,
"Update with invalid data should fail script validation");
println!("Script Validation on Update: Correctly rejected invalid update");
}
}
}
}
}
// ========================================================================
// COMPREHENSIVE DATA TYPE TESTS
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_boolean_data_type(#[future] form_test_context: FormTestContext) {
let mut context = form_test_context.await;
skip_if_backend_unavailable!();
// Test valid boolean values
let boolean_test_cases = vec![
("true", true),
("false", false),
];
for (case_name, bool_value) in boolean_test_cases {
let mut data = HashMap::new();
data.insert("firma".to_string(), create_string_value("Boolean Test Company"));
// Assuming there's a boolean field called 'active'
data.insert("active".to_string(), create_bool_value(bool_value));
let result = context.client.post_table_data(
context.profile_name.clone(),
context.table_name.clone(),
data,
).await;
match result {
Ok(response) => {
println!("Boolean Test: {} value succeeded", case_name);
// Verify the value round-trip
if let Ok(get_response) = context.client.get_table_data(
context.profile_name.clone(),
context.table_name.clone(),
response.inserted_id,
).await {
if let Some(retrieved_value) = get_response.data.get("active") {
println!("Boolean Test: {} round-trip value: {}", case_name, retrieved_value);
}
}
}
Err(e) => {
println!("Boolean Test: {} failed (field may not exist): {}", case_name, e);
}
}
}
}
#[rstest]
#[tokio::test]
async fn test_numeric_data_types(#[future] form_test_context: FormTestContext) {
let mut context = form_test_context.await;
skip_if_backend_unavailable!();
// Test various numeric values
let numeric_test_cases = vec![
("Zero", 0.0),
("Positive", 123.45),
("Negative", -67.89),
("Large", 999999.99),
("SmallDecimal", 0.01),
];
for (case_name, numeric_value) in numeric_test_cases {
let mut data = HashMap::new();
data.insert("firma".to_string(), create_string_value("Numeric Test Company"));
// Assuming there's a numeric field called 'price' or 'amount'
data.insert("amount".to_string(), create_number_value(numeric_value));
let result = context.client.post_table_data(
context.profile_name.clone(),
context.table_name.clone(),
data,
).await;
match result {
Ok(response) => {
println!("Numeric Test: {} ({}) succeeded", case_name, numeric_value);
// Verify round-trip
if let Ok(get_response) = context.client.get_table_data(
context.profile_name.clone(),
context.table_name.clone(),
response.inserted_id,
).await {
if let Some(retrieved_value) = get_response.data.get("amount") {
println!("Numeric Test: {} round-trip value: {}", case_name, retrieved_value);
}
}
}
Err(e) => {
println!("Numeric Test: {} failed (field may not exist): {}", case_name, e);
}
}
}
}
#[rstest]
#[tokio::test]
async fn test_timestamp_data_type(#[future] form_test_context: FormTestContext) {
let mut context = form_test_context.await;
skip_if_backend_unavailable!();
// Test various timestamp formats
let timestamp_test_cases = vec![
("ISO8601", "2024-01-15T10:30:00Z"),
("WithTimezone", "2024-01-15T10:30:00+01:00"),
("WithMilliseconds", "2024-01-15T10:30:00.123Z"),
];
for (case_name, timestamp_str) in timestamp_test_cases {
let mut data = HashMap::new();
data.insert("firma".to_string(), create_string_value("Timestamp Test Company"));
// Assuming there's a timestamp field called 'created_at'
data.insert("created_at".to_string(), create_string_value(timestamp_str));
let result = context.client.post_table_data(
context.profile_name.clone(),
context.table_name.clone(),
data,
).await;
match result {
Ok(response) => {
println!("Timestamp Test: {} succeeded", case_name);
// Verify round-trip
if let Ok(get_response) = context.client.get_table_data(
context.profile_name.clone(),
context.table_name.clone(),
response.inserted_id,
).await {
if let Some(retrieved_value) = get_response.data.get("created_at") {
println!("Timestamp Test: {} round-trip value: {}", case_name, retrieved_value);
}
}
}
Err(e) => {
println!("Timestamp Test: {} failed (field may not exist): {}", case_name, e);
}
}
}
}
#[rstest]
#[tokio::test]
async fn test_invalid_data_types(#[future] form_test_context: FormTestContext) {
let mut context = form_test_context.await;
skip_if_backend_unavailable!();
// Test invalid data type combinations
let invalid_type_cases = vec![
("StringForNumber", "amount", create_string_value("not-a-number")),
("NumberForBoolean", "active", create_number_value(123.0)),
("StringForBoolean", "active", create_string_value("maybe")),
("InvalidTimestamp", "created_at", create_string_value("not-a-date")),
];
for (case_name, field_name, invalid_value) in invalid_type_cases {
let mut data = HashMap::new();
data.insert("firma".to_string(), create_string_value("Invalid Type Test"));
data.insert(field_name.to_string(), invalid_value);
let result = context.client.post_table_data(
context.profile_name.clone(),
context.table_name.clone(),
data,
).await;
match result {
Ok(_) => {
println!("Invalid Type Test: {} passed (no type validation or field doesn't exist)", case_name);
}
Err(e) => {
if let Some(status) = e.downcast_ref::<Status>() {
assert_eq!(status.code(), tonic::Code::InvalidArgument,
"Invalid data type should return InvalidArgument for case: {}", case_name);
println!("Invalid Type Test: {} correctly rejected - {}", case_name, status.message());
}
}
}
}
}
// ========================================================================
// FOREIGN KEY RELATIONSHIP TESTS
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_foreign_key_valid_relationship(#[future] form_test_context: FormTestContext) {
let mut context = form_test_context.await;
skip_if_backend_unavailable!();
// 1. Create a parent record first (e.g., company)
let mut parent_data = HashMap::new();
parent_data.insert("firma".to_string(), create_string_value("Parent Company"));
let parent_result = context.client.post_table_data(
context.profile_name.clone(),
"companies".to_string(), // Assuming companies table exists
parent_data,
).await;
if let Ok(parent_response) = parent_result {
let parent_id = parent_response.inserted_id;
// 2. Create a child record that references the parent
let mut child_data = HashMap::new();
child_data.insert("name".to_string(), create_string_value("Child Record"));
child_data.insert("company_id".to_string(), create_number_value(parent_id as f64));
let child_result = context.client.post_table_data(
context.profile_name.clone(),
"contacts".to_string(), // Assuming contacts table exists
child_data,
).await;
match child_result {
Ok(child_response) => {
assert!(child_response.success, "Valid foreign key relationship should succeed");
println!("Foreign Key Test: Valid relationship created - Parent ID: {}, Child ID: {}",
parent_id, child_response.inserted_id);
}
Err(e) => {
println!("Foreign Key Test: Failed (tables may not exist or no FK constraint): {}", e);
}
}
} else {
println!("Foreign Key Test: Could not create parent record");
}
}
#[rstest]
#[tokio::test]
async fn test_foreign_key_invalid_relationship(#[future] form_test_context: FormTestContext) {
let mut context = form_test_context.await;
skip_if_backend_unavailable!();
// Try to create a child record with non-existent parent ID
let mut invalid_child_data = HashMap::new();
invalid_child_data.insert("name".to_string(), create_string_value("Orphan Record"));
invalid_child_data.insert("company_id".to_string(), create_number_value(99999.0)); // Non-existent ID
let result = context.client.post_table_data(
context.profile_name.clone(),
"contacts".to_string(),
invalid_child_data,
).await;
match result {
Ok(_) => {
println!("Foreign Key Test: Invalid relationship passed (no FK constraint configured)");
}
Err(e) => {
if let Some(status) = e.downcast_ref::<Status>() {
// Could be InvalidArgument or NotFound depending on implementation
assert!(matches!(status.code(), tonic::Code::InvalidArgument | tonic::Code::NotFound),
"Invalid foreign key should return InvalidArgument or NotFound");
println!("Foreign Key Test: Invalid relationship correctly rejected - {}", status.message());
}
}
}
}
// ========================================================================
// DELETED RECORD INTERACTION TESTS
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_update_deleted_record_behavior(#[future] form_test_context: FormTestContext) {
let mut context = form_test_context.await;
skip_if_backend_unavailable!();
// 1. Create a record
let initial_data = context.create_test_form_data();
let post_result = context.client.post_table_data(
context.profile_name.clone(),
context.table_name.clone(),
initial_data,
).await;
if let Ok(post_response) = post_result {
let record_id = post_response.inserted_id;
println!("Deleted Record Test: Created record ID {}", record_id);
// 2. Delete the record (soft delete)
let delete_result = context.client.delete_table_data(
context.profile_name.clone(),
context.table_name.clone(),
record_id,
).await;
assert!(delete_result.is_ok(), "Delete should succeed");
println!("Deleted Record Test: Soft-deleted record {}", record_id);
// 3. Try to UPDATE the deleted record
let mut update_data = HashMap::new();
update_data.insert("firma".to_string(), create_string_value("Updated Deleted Record"));
let update_result = context.client.put_table_data(
context.profile_name.clone(),
context.table_name.clone(),
record_id,
update_data,
).await;
match update_result {
Ok(_) => {
// This might be a bug - updating deleted records should probably fail
println!("Deleted Record Test: UPDATE on deleted record succeeded (potential bug?)");
// Check if the record is still considered deleted
let get_result = context.client.get_table_data(
context.profile_name.clone(),
context.table_name.clone(),
record_id,
).await;
if get_result.is_err() {
println!("Deleted Record Test: Record still appears deleted after update");
} else {
println!("Deleted Record Test: Record appears to be undeleted after update");
}
}
Err(e) => {
if let Some(status) = e.downcast_ref::<Status>() {
assert_eq!(status.code(), tonic::Code::NotFound,
"UPDATE on deleted record should return NotFound");
println!("Deleted Record Test: UPDATE correctly rejected on deleted record");
}
}
}
}
}
#[rstest]
#[tokio::test]
async fn test_delete_already_deleted_record(#[future] form_test_context: FormTestContext) {
let mut context = form_test_context.await;
skip_if_backend_unavailable!();
// 1. Create and delete a record
let initial_data = context.create_test_form_data();
let post_result = context.client.post_table_data(
context.profile_name.clone(),
context.table_name.clone(),
initial_data,
).await;
if let Ok(post_response) = post_result {
let record_id = post_response.inserted_id;
// First deletion
let delete_result1 = context.client.delete_table_data(
context.profile_name.clone(),
context.table_name.clone(),
record_id,
).await;
assert!(delete_result1.is_ok(), "First delete should succeed");
// Second deletion (idempotent)
let delete_result2 = context.client.delete_table_data(
context.profile_name.clone(),
context.table_name.clone(),
record_id,
).await;
assert!(delete_result2.is_ok(), "Second delete should succeed (idempotent)");
if let Ok(response) = delete_result2 {
assert!(response.success, "Delete should report success even for already-deleted record");
}
println!("Double Delete Test: Both deletions succeeded (idempotent behavior)");
}
}
// ========================================================================
// VALIDATION AND BOUNDARY TESTS
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_large_data_handling(#[future] form_test_context: FormTestContext) {
let mut context = form_test_context.await;
skip_if_backend_unavailable!();
// Test with very large string values
let large_string = "A".repeat(10000); // 10KB string
let very_large_string = "B".repeat(100000); // 100KB string
let test_cases = vec![
("Large", large_string),
("VeryLarge", very_large_string),
];
for (case_name, large_value) in test_cases {
let mut data = HashMap::new();
data.insert("firma".to_string(), create_string_value(&large_value));
let result = context.client.post_table_data(
context.profile_name.clone(),
context.table_name.clone(),
data,
).await;
match result {
Ok(response) => {
println!("Large Data Test: {} string handled successfully", case_name);
// Verify round-trip
if let Ok(get_response) = context.client.get_table_data(
context.profile_name.clone(),
context.table_name.clone(),
response.inserted_id,
).await {
if let Some(retrieved_value) = get_response.data.get("firma") {
assert_eq!(retrieved_value.len(), large_value.len(),
"Large string should survive round-trip for case: {}", case_name);
}
}
}
Err(e) => {
println!("Large Data Test: {} failed (may hit size limits): {}", case_name, e);
}
}
}
}
#[rstest]
#[tokio::test]
async fn test_sql_injection_attempts(#[future] form_test_context: FormTestContext) {
let mut context = form_test_context.await;
skip_if_backend_unavailable!();
// Test potential SQL injection strings
let injection_attempts = vec![
("SingleQuote", "'; DROP TABLE users; --"),
("DoubleQuote", "\"; DROP TABLE users; --"),
("Union", "' UNION SELECT * FROM users --"),
("Comment", "/* malicious comment */"),
("Semicolon", "; DELETE FROM users;"),
];
for (case_name, injection_string) in injection_attempts {
let mut data = HashMap::new();
data.insert("firma".to_string(), create_string_value(injection_string));
data.insert("kz".to_string(), create_string_value("KZ123"));
let result = context.client.post_table_data(
context.profile_name.clone(),
context.table_name.clone(),
data,
).await;
match result {
Ok(response) => {
println!("SQL Injection Test: {} handled safely (parameterized queries)", case_name);
// Verify the malicious string was stored as-is (not executed)
if let Ok(get_response) = context.client.get_table_data(
context.profile_name.clone(),
context.table_name.clone(),
response.inserted_id,
).await {
if let Some(retrieved_value) = get_response.data.get("firma") {
assert_eq!(retrieved_value, injection_string,
"Injection string should be stored literally for case: {}", case_name);
}
}
}
Err(e) => {
println!("SQL Injection Test: {} rejected: {}", case_name, e);
}
}
}
}
#[rstest]
#[tokio::test]
async fn test_concurrent_operations_with_same_data(#[future] form_test_context: FormTestContext) {
let context = form_test_context.await;
skip_if_backend_unavailable!();
// Test multiple concurrent operations with identical data
let mut handles = Vec::new();
let num_tasks = 10;
for i in 0..num_tasks {
let mut context_clone = context.clone();
let handle = tokio::spawn(async move {
let mut data = HashMap::new();
data.insert("firma".to_string(), create_string_value("Concurrent Identical"));
data.insert("kz".to_string(), create_string_value(&format!("SAME{:02}", i)));
context_clone.client.post_table_data(
context_clone.profile_name,
context_clone.table_name,
data,
).await
});
handles.push(handle);
}
// Wait for all to complete
let mut success_count = 0;
let mut inserted_ids = Vec::new();
for (i, handle) in handles.into_iter().enumerate() {
match handle.await {
Ok(Ok(response)) => {
success_count += 1;
inserted_ids.push(response.inserted_id);
println!("Concurrent Identical Data: Task {} succeeded with ID {}", i, response.inserted_id);
}
Ok(Err(e)) => {
println!("Concurrent Identical Data: Task {} failed: {}", i, e);
}
Err(e) => {
println!("Concurrent Identical Data: Task {} panicked: {}", i, e);
}
}
}
assert!(success_count > 0, "At least some concurrent operations should succeed");
// Verify all IDs are unique
let unique_ids: std::collections::HashSet<_> = inserted_ids.iter().collect();
assert_eq!(unique_ids.len(), inserted_ids.len(), "All inserted IDs should be unique");
println!("Concurrent Identical Data: {}/{} operations succeeded with unique IDs",
success_count, num_tasks);
}
// ========================================================================
// PERFORMANCE AND STRESS TESTS
// ========================================================================
#[rstest]
#[tokio::test]
async fn test_bulk_operations_performance(#[future] form_test_context: FormTestContext) {
let mut context = form_test_context.await;
skip_if_backend_unavailable!();
let operation_count = 50;
let start_time = std::time::Instant::now();
let mut successful_operations = 0;
let mut created_ids = Vec::new();
// Bulk create
for i in 0..operation_count {
let mut data = HashMap::new();
data.insert("firma".to_string(), create_string_value(&format!("Bulk Company {}", i)));
data.insert("kz".to_string(), create_string_value(&format!("BLK{:02}", i)));
if let Ok(response) = context.client.post_table_data(
context.profile_name.clone(),
context.table_name.clone(),
data,
).await {
successful_operations += 1;
created_ids.push(response.inserted_id);
}
}
let create_duration = start_time.elapsed();
println!("Bulk Performance: Created {} records in {:?}", successful_operations, create_duration);
// Bulk read
let read_start = std::time::Instant::now();
let mut successful_reads = 0;
for &record_id in &created_ids {
if context.client.get_table_data(
context.profile_name.clone(),
context.table_name.clone(),
record_id,
).await.is_ok() {
successful_reads += 1;
}
}
let read_duration = read_start.elapsed();
println!("Bulk Performance: Read {} records in {:?}", successful_reads, read_duration);
// Performance assertions
assert!(successful_operations > operation_count * 8 / 10,
"At least 80% of operations should succeed");
assert!(create_duration.as_secs() < 60,
"Bulk operations should complete in reasonable time");
println!("Bulk Performance Test: {}/{} creates, {}/{} reads successful",
successful_operations, operation_count, successful_reads, created_ids.len());
}

View File

@@ -0,0 +1 @@
pub mod form_request_tests;

3
client/tests/mod.rs Normal file
View File

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

View File

@@ -1,6 +1,6 @@
// proto/adresar.proto
syntax = "proto3";
package multieko2.adresar;
package komp_ac.adresar;
import "common.proto";
// import "table_structure.proto";

View File

@@ -1,6 +1,6 @@
// proto/auth.proto
syntax = "proto3";
package multieko2.auth;
package komp_ac.auth;
import "common.proto";

View File

@@ -1,6 +1,6 @@
// proto/common.proto
syntax = "proto3";
package multieko2.common;
package komp_ac.common;
message Empty {}
message CountResponse { int64 count = 1; }

View File

@@ -1,6 +1,6 @@
// In common/proto/search.proto
syntax = "proto3";
package multieko2.search;
package komp_ac.search;
service Searcher {
rpc SearchTable(SearchRequest) returns (SearchResponse);

View File

@@ -1,12 +1,12 @@
// common/proto/table_definition.proto
syntax = "proto3";
package multieko2.table_definition;
package komp_ac.table_definition;
import "common.proto";
service TableDefinition {
rpc PostTableDefinition (PostTableDefinitionRequest) returns (TableDefinitionResponse);
rpc GetProfileTree (multieko2.common.Empty) returns (ProfileTreeResponse);
rpc GetProfileTree (komp_ac.common.Empty) returns (ProfileTreeResponse);
rpc DeleteTable (DeleteTableRequest) returns (DeleteTableResponse);
}

View File

@@ -1,5 +1,5 @@
syntax = "proto3";
package multieko2.table_script;
package komp_ac.table_script;
service TableScript {
rpc PostTableScript(PostTableScriptRequest) returns (TableScriptResponse);

View File

@@ -1,6 +1,6 @@
// proto/table_structure.proto
syntax = "proto3";
package multieko2.table_structure;
package komp_ac.table_structure;
import "common.proto";

View File

@@ -1,6 +1,6 @@
// common/proto/tables_data.proto
syntax = "proto3";
package multieko2.tables_data;
package komp_ac.tables_data;
import "common.proto";
import "google/protobuf/struct.proto";
@@ -10,7 +10,7 @@ service TablesData {
rpc PutTableData (PutTableDataRequest) returns (PutTableDataResponse);
rpc DeleteTableData (DeleteTableDataRequest) returns (DeleteTableDataResponse);
rpc GetTableData(GetTableDataRequest) returns (GetTableDataResponse);
rpc GetTableDataCount(GetTableDataCountRequest) returns (multieko2.common.CountResponse);
rpc GetTableDataCount(GetTableDataCountRequest) returns (komp_ac.common.CountResponse);
rpc GetTableDataByPosition(GetTableDataByPositionRequest) returns (GetTableDataResponse);
}

View File

@@ -1,6 +1,6 @@
// proto/uctovnictvo.proto
syntax = "proto3";
package multieko2.uctovnictvo;
package komp_ac.uctovnictvo;
import "common.proto";

View File

@@ -3,33 +3,33 @@
pub mod search;
pub mod proto {
pub mod multieko2 {
pub mod komp_ac {
pub mod adresar {
include!("proto/multieko2.adresar.rs");
include!("proto/komp_ac.adresar.rs");
}
pub mod auth {
include!("proto/multieko2.auth.rs");
include!("proto/komp_ac.auth.rs");
}
pub mod common {
include!("proto/multieko2.common.rs");
include!("proto/komp_ac.common.rs");
}
pub mod table_structure {
include!("proto/multieko2.table_structure.rs");
include!("proto/komp_ac.table_structure.rs");
}
pub mod uctovnictvo {
include!("proto/multieko2.uctovnictvo.rs");
include!("proto/komp_ac.uctovnictvo.rs");
}
pub mod table_definition {
include!("proto/multieko2.table_definition.rs");
include!("proto/komp_ac.table_definition.rs");
}
pub mod tables_data {
include!("proto/multieko2.tables_data.rs");
include!("proto/komp_ac.tables_data.rs");
}
pub mod table_script {
include!("proto/multieko2.table_script.rs");
include!("proto/komp_ac.table_script.rs");
}
pub mod search {
include!("proto/multieko2.search.rs");
include!("proto/komp_ac.search.rs");
}
pub const FILE_DESCRIPTOR_SET: &[u8] =
include_bytes!("proto/descriptor.bin");

Binary file not shown.

View File

@@ -0,0 +1,791 @@
// 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

@@ -0,0 +1,418 @@
// 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

@@ -0,0 +1,13 @@
// 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

@@ -0,0 +1,317 @@
// 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

@@ -0,0 +1,544 @@
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TableLink {
#[prost(string, tag = "1")]
pub linked_table_name: ::prost::alloc::string::String,
#[prost(bool, tag = "2")]
pub required: bool,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PostTableDefinitionRequest {
#[prost(string, tag = "1")]
pub table_name: ::prost::alloc::string::String,
#[prost(message, repeated, tag = "2")]
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")]
pub field_type: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TableDefinitionResponse {
#[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")]
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 DeleteTableResponse {
#[prost(bool, tag = "1")]
pub success: bool,
#[prost(string, tag = "2")]
pub message: ::prost::alloc::string::String,
}
/// Generated client implementations.
pub mod table_definition_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 TableDefinitionClient<T> {
inner: tonic::client::Grpc<T>,
}
impl TableDefinitionClient<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> TableDefinitionClient<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,
) -> TableDefinitionClient<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,
{
TableDefinitionClient::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_definition(
&mut self,
request: impl tonic::IntoRequest<super::PostTableDefinitionRequest>,
) -> std::result::Result<
tonic::Response<super::TableDefinitionResponse>,
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/PostTableDefinition",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"komp_ac.table_definition.TableDefinition",
"PostTableDefinition",
),
);
self.inner.unary(req, path, codec).await
}
pub async fn get_profile_tree(
&mut self,
request: impl tonic::IntoRequest<super::super::common::Empty>,
) -> std::result::Result<
tonic::Response<super::ProfileTreeResponse>,
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/GetProfileTree",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"komp_ac.table_definition.TableDefinition",
"GetProfileTree",
),
);
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
}
}
}
/// Generated server implementations.
pub mod table_definition_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 TableDefinitionServer.
#[async_trait]
pub trait TableDefinition: std::marker::Send + std::marker::Sync + 'static {
async fn post_table_definition(
&self,
request: tonic::Request<super::PostTableDefinitionRequest>,
) -> std::result::Result<
tonic::Response<super::TableDefinitionResponse>,
tonic::Status,
>;
async fn get_profile_tree(
&self,
request: tonic::Request<super::super::common::Empty>,
) -> std::result::Result<
tonic::Response<super::ProfileTreeResponse>,
tonic::Status,
>;
async fn delete_table(
&self,
request: tonic::Request<super::DeleteTableRequest>,
) -> std::result::Result<
tonic::Response<super::DeleteTableResponse>,
tonic::Status,
>;
}
#[derive(Debug)]
pub struct TableDefinitionServer<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> TableDefinitionServer<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 TableDefinitionServer<T>
where
T: TableDefinition,
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_definition.TableDefinition/PostTableDefinition" => {
#[allow(non_camel_case_types)]
struct PostTableDefinitionSvc<T: TableDefinition>(pub Arc<T>);
impl<
T: TableDefinition,
> tonic::server::UnaryService<super::PostTableDefinitionRequest>
for PostTableDefinitionSvc<T> {
type Response = super::TableDefinitionResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::PostTableDefinitionRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as TableDefinition>::post_table_definition(
&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 = PostTableDefinitionSvc(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/GetProfileTree" => {
#[allow(non_camel_case_types)]
struct GetProfileTreeSvc<T: TableDefinition>(pub Arc<T>);
impl<
T: TableDefinition,
> tonic::server::UnaryService<super::super::common::Empty>
for GetProfileTreeSvc<T> {
type Response = super::ProfileTreeResponse;
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 TableDefinition>::get_profile_tree(&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 = GetProfileTreeSvc(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 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 TableDefinitionServer<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_definition.TableDefinition";
impl<T> tonic::server::NamedService for TableDefinitionServer<T> {
const NAME: &'static str = SERVICE_NAME;
}
}

View File

@@ -0,0 +1,323 @@
// 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

@@ -0,0 +1,336 @@
// 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

@@ -0,0 +1,794 @@
// 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

@@ -0,0 +1,712 @@
// 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

@@ -225,11 +225,11 @@ pub mod adresar_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.adresar.Adresar/PostAdresar",
"/komp_ac.adresar.Adresar/PostAdresar",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("multieko2.adresar.Adresar", "PostAdresar"));
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "PostAdresar"));
self.inner.unary(req, path, codec).await
}
pub async fn get_adresar(
@@ -249,11 +249,11 @@ pub mod adresar_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.adresar.Adresar/GetAdresar",
"/komp_ac.adresar.Adresar/GetAdresar",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("multieko2.adresar.Adresar", "GetAdresar"));
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "GetAdresar"));
self.inner.unary(req, path, codec).await
}
pub async fn put_adresar(
@@ -273,11 +273,11 @@ pub mod adresar_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.adresar.Adresar/PutAdresar",
"/komp_ac.adresar.Adresar/PutAdresar",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("multieko2.adresar.Adresar", "PutAdresar"));
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "PutAdresar"));
self.inner.unary(req, path, codec).await
}
pub async fn delete_adresar(
@@ -297,11 +297,11 @@ pub mod adresar_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.adresar.Adresar/DeleteAdresar",
"/komp_ac.adresar.Adresar/DeleteAdresar",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("multieko2.adresar.Adresar", "DeleteAdresar"));
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "DeleteAdresar"));
self.inner.unary(req, path, codec).await
}
pub async fn get_adresar_count(
@@ -321,11 +321,11 @@ pub mod adresar_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.adresar.Adresar/GetAdresarCount",
"/komp_ac.adresar.Adresar/GetAdresarCount",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("multieko2.adresar.Adresar", "GetAdresarCount"));
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "GetAdresarCount"));
self.inner.unary(req, path, codec).await
}
pub async fn get_adresar_by_position(
@@ -345,12 +345,12 @@ pub mod adresar_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.adresar.Adresar/GetAdresarByPosition",
"/komp_ac.adresar.Adresar/GetAdresarByPosition",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new("multieko2.adresar.Adresar", "GetAdresarByPosition"),
GrpcMethod::new("komp_ac.adresar.Adresar", "GetAdresarByPosition"),
);
self.inner.unary(req, path, codec).await
}
@@ -476,7 +476,7 @@ pub mod adresar_server {
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/multieko2.adresar.Adresar/PostAdresar" => {
"/komp_ac.adresar.Adresar/PostAdresar" => {
#[allow(non_camel_case_types)]
struct PostAdresarSvc<T: Adresar>(pub Arc<T>);
impl<
@@ -521,7 +521,7 @@ pub mod adresar_server {
};
Box::pin(fut)
}
"/multieko2.adresar.Adresar/GetAdresar" => {
"/komp_ac.adresar.Adresar/GetAdresar" => {
#[allow(non_camel_case_types)]
struct GetAdresarSvc<T: Adresar>(pub Arc<T>);
impl<
@@ -566,7 +566,7 @@ pub mod adresar_server {
};
Box::pin(fut)
}
"/multieko2.adresar.Adresar/PutAdresar" => {
"/komp_ac.adresar.Adresar/PutAdresar" => {
#[allow(non_camel_case_types)]
struct PutAdresarSvc<T: Adresar>(pub Arc<T>);
impl<
@@ -611,7 +611,7 @@ pub mod adresar_server {
};
Box::pin(fut)
}
"/multieko2.adresar.Adresar/DeleteAdresar" => {
"/komp_ac.adresar.Adresar/DeleteAdresar" => {
#[allow(non_camel_case_types)]
struct DeleteAdresarSvc<T: Adresar>(pub Arc<T>);
impl<
@@ -656,7 +656,7 @@ pub mod adresar_server {
};
Box::pin(fut)
}
"/multieko2.adresar.Adresar/GetAdresarCount" => {
"/komp_ac.adresar.Adresar/GetAdresarCount" => {
#[allow(non_camel_case_types)]
struct GetAdresarCountSvc<T: Adresar>(pub Arc<T>);
impl<
@@ -701,7 +701,7 @@ pub mod adresar_server {
};
Box::pin(fut)
}
"/multieko2.adresar.Adresar/GetAdresarByPosition" => {
"/komp_ac.adresar.Adresar/GetAdresarByPosition" => {
#[allow(non_camel_case_types)]
struct GetAdresarByPositionSvc<T: Adresar>(pub Arc<T>);
impl<
@@ -784,7 +784,7 @@ pub mod adresar_server {
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "multieko2.adresar.Adresar";
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

@@ -160,11 +160,11 @@ pub mod auth_service_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.auth.AuthService/Register",
"/komp_ac.auth.AuthService/Register",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("multieko2.auth.AuthService", "Register"));
.insert(GrpcMethod::new("komp_ac.auth.AuthService", "Register"));
self.inner.unary(req, path, codec).await
}
pub async fn login(
@@ -181,11 +181,11 @@ pub mod auth_service_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.auth.AuthService/Login",
"/komp_ac.auth.AuthService/Login",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("multieko2.auth.AuthService", "Login"));
.insert(GrpcMethod::new("komp_ac.auth.AuthService", "Login"));
self.inner.unary(req, path, codec).await
}
}
@@ -288,7 +288,7 @@ pub mod auth_service_server {
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/multieko2.auth.AuthService/Register" => {
"/komp_ac.auth.AuthService/Register" => {
#[allow(non_camel_case_types)]
struct RegisterSvc<T: AuthService>(pub Arc<T>);
impl<
@@ -333,7 +333,7 @@ pub mod auth_service_server {
};
Box::pin(fut)
}
"/multieko2.auth.AuthService/Login" => {
"/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>
@@ -411,7 +411,7 @@ pub mod auth_service_server {
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "multieko2.auth.AuthService";
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

@@ -129,11 +129,11 @@ pub mod searcher_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.search.Searcher/SearchTable",
"/komp_ac.search.Searcher/SearchTable",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("multieko2.search.Searcher", "SearchTable"));
.insert(GrpcMethod::new("komp_ac.search.Searcher", "SearchTable"));
self.inner.unary(req, path, codec).await
}
}
@@ -232,7 +232,7 @@ pub mod searcher_server {
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/multieko2.search.Searcher/SearchTable" => {
"/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>
@@ -310,7 +310,7 @@ pub mod searcher_server {
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "multieko2.search.Searcher";
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

@@ -179,13 +179,13 @@ pub mod table_definition_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.table_definition.TableDefinition/PostTableDefinition",
"/komp_ac.table_definition.TableDefinition/PostTableDefinition",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.table_definition.TableDefinition",
"komp_ac.table_definition.TableDefinition",
"PostTableDefinition",
),
);
@@ -208,13 +208,13 @@ pub mod table_definition_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.table_definition.TableDefinition/GetProfileTree",
"/komp_ac.table_definition.TableDefinition/GetProfileTree",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.table_definition.TableDefinition",
"komp_ac.table_definition.TableDefinition",
"GetProfileTree",
),
);
@@ -237,13 +237,13 @@ pub mod table_definition_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.table_definition.TableDefinition/DeleteTable",
"/komp_ac.table_definition.TableDefinition/DeleteTable",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.table_definition.TableDefinition",
"komp_ac.table_definition.TableDefinition",
"DeleteTable",
),
);
@@ -362,7 +362,7 @@ pub mod table_definition_server {
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/multieko2.table_definition.TableDefinition/PostTableDefinition" => {
"/komp_ac.table_definition.TableDefinition/PostTableDefinition" => {
#[allow(non_camel_case_types)]
struct PostTableDefinitionSvc<T: TableDefinition>(pub Arc<T>);
impl<
@@ -411,7 +411,7 @@ pub mod table_definition_server {
};
Box::pin(fut)
}
"/multieko2.table_definition.TableDefinition/GetProfileTree" => {
"/komp_ac.table_definition.TableDefinition/GetProfileTree" => {
#[allow(non_camel_case_types)]
struct GetProfileTreeSvc<T: TableDefinition>(pub Arc<T>);
impl<
@@ -457,7 +457,7 @@ pub mod table_definition_server {
};
Box::pin(fut)
}
"/multieko2.table_definition.TableDefinition/DeleteTable" => {
"/komp_ac.table_definition.TableDefinition/DeleteTable" => {
#[allow(non_camel_case_types)]
struct DeleteTableSvc<T: TableDefinition>(pub Arc<T>);
impl<
@@ -537,7 +537,7 @@ pub mod table_definition_server {
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "multieko2.table_definition.TableDefinition";
pub const SERVICE_NAME: &str = "komp_ac.table_definition.TableDefinition";
impl<T> tonic::server::NamedService for TableDefinitionServer<T> {
const NAME: &'static str = SERVICE_NAME;
}

View File

@@ -125,13 +125,13 @@ pub mod table_script_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.table_script.TableScript/PostTableScript",
"/komp_ac.table_script.TableScript/PostTableScript",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.table_script.TableScript",
"komp_ac.table_script.TableScript",
"PostTableScript",
),
);
@@ -236,7 +236,7 @@ pub mod table_script_server {
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/multieko2.table_script.TableScript/PostTableScript" => {
"/komp_ac.table_script.TableScript/PostTableScript" => {
#[allow(non_camel_case_types)]
struct PostTableScriptSvc<T: TableScript>(pub Arc<T>);
impl<
@@ -316,7 +316,7 @@ pub mod table_script_server {
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "multieko2.table_script.TableScript";
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

@@ -133,13 +133,13 @@ pub mod table_structure_service_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.table_structure.TableStructureService/GetTableStructure",
"/komp_ac.table_structure.TableStructureService/GetTableStructure",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.table_structure.TableStructureService",
"komp_ac.table_structure.TableStructureService",
"GetTableStructure",
),
);
@@ -245,7 +245,7 @@ pub mod table_structure_service_server {
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/multieko2.table_structure.TableStructureService/GetTableStructure" => {
"/komp_ac.table_structure.TableStructureService/GetTableStructure" => {
#[allow(non_camel_case_types)]
struct GetTableStructureSvc<T: TableStructureService>(pub Arc<T>);
impl<
@@ -329,7 +329,7 @@ pub mod table_structure_service_server {
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "multieko2.table_structure.TableStructureService";
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

@@ -198,12 +198,12 @@ pub mod tables_data_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.tables_data.TablesData/PostTableData",
"/komp_ac.tables_data.TablesData/PostTableData",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new("multieko2.tables_data.TablesData", "PostTableData"),
GrpcMethod::new("komp_ac.tables_data.TablesData", "PostTableData"),
);
self.inner.unary(req, path, codec).await
}
@@ -224,12 +224,12 @@ pub mod tables_data_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.tables_data.TablesData/PutTableData",
"/komp_ac.tables_data.TablesData/PutTableData",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new("multieko2.tables_data.TablesData", "PutTableData"),
GrpcMethod::new("komp_ac.tables_data.TablesData", "PutTableData"),
);
self.inner.unary(req, path, codec).await
}
@@ -250,13 +250,13 @@ pub mod tables_data_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.tables_data.TablesData/DeleteTableData",
"/komp_ac.tables_data.TablesData/DeleteTableData",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.tables_data.TablesData",
"komp_ac.tables_data.TablesData",
"DeleteTableData",
),
);
@@ -279,12 +279,12 @@ pub mod tables_data_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.tables_data.TablesData/GetTableData",
"/komp_ac.tables_data.TablesData/GetTableData",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new("multieko2.tables_data.TablesData", "GetTableData"),
GrpcMethod::new("komp_ac.tables_data.TablesData", "GetTableData"),
);
self.inner.unary(req, path, codec).await
}
@@ -305,13 +305,13 @@ pub mod tables_data_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.tables_data.TablesData/GetTableDataCount",
"/komp_ac.tables_data.TablesData/GetTableDataCount",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.tables_data.TablesData",
"komp_ac.tables_data.TablesData",
"GetTableDataCount",
),
);
@@ -334,13 +334,13 @@ pub mod tables_data_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.tables_data.TablesData/GetTableDataByPosition",
"/komp_ac.tables_data.TablesData/GetTableDataByPosition",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.tables_data.TablesData",
"komp_ac.tables_data.TablesData",
"GetTableDataByPosition",
),
);
@@ -480,7 +480,7 @@ pub mod tables_data_server {
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/multieko2.tables_data.TablesData/PostTableData" => {
"/komp_ac.tables_data.TablesData/PostTableData" => {
#[allow(non_camel_case_types)]
struct PostTableDataSvc<T: TablesData>(pub Arc<T>);
impl<
@@ -525,7 +525,7 @@ pub mod tables_data_server {
};
Box::pin(fut)
}
"/multieko2.tables_data.TablesData/PutTableData" => {
"/komp_ac.tables_data.TablesData/PutTableData" => {
#[allow(non_camel_case_types)]
struct PutTableDataSvc<T: TablesData>(pub Arc<T>);
impl<
@@ -570,7 +570,7 @@ pub mod tables_data_server {
};
Box::pin(fut)
}
"/multieko2.tables_data.TablesData/DeleteTableData" => {
"/komp_ac.tables_data.TablesData/DeleteTableData" => {
#[allow(non_camel_case_types)]
struct DeleteTableDataSvc<T: TablesData>(pub Arc<T>);
impl<
@@ -615,7 +615,7 @@ pub mod tables_data_server {
};
Box::pin(fut)
}
"/multieko2.tables_data.TablesData/GetTableData" => {
"/komp_ac.tables_data.TablesData/GetTableData" => {
#[allow(non_camel_case_types)]
struct GetTableDataSvc<T: TablesData>(pub Arc<T>);
impl<
@@ -660,7 +660,7 @@ pub mod tables_data_server {
};
Box::pin(fut)
}
"/multieko2.tables_data.TablesData/GetTableDataCount" => {
"/komp_ac.tables_data.TablesData/GetTableDataCount" => {
#[allow(non_camel_case_types)]
struct GetTableDataCountSvc<T: TablesData>(pub Arc<T>);
impl<
@@ -706,7 +706,7 @@ pub mod tables_data_server {
};
Box::pin(fut)
}
"/multieko2.tables_data.TablesData/GetTableDataByPosition" => {
"/komp_ac.tables_data.TablesData/GetTableDataByPosition" => {
#[allow(non_camel_case_types)]
struct GetTableDataByPositionSvc<T: TablesData>(pub Arc<T>);
impl<
@@ -790,7 +790,7 @@ pub mod tables_data_server {
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "multieko2.tables_data.TablesData";
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

@@ -192,13 +192,13 @@ pub mod uctovnictvo_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.uctovnictvo.Uctovnictvo/PostUctovnictvo",
"/komp_ac.uctovnictvo.Uctovnictvo/PostUctovnictvo",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.uctovnictvo.Uctovnictvo",
"komp_ac.uctovnictvo.Uctovnictvo",
"PostUctovnictvo",
),
);
@@ -221,13 +221,13 @@ pub mod uctovnictvo_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.uctovnictvo.Uctovnictvo/GetUctovnictvo",
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvo",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.uctovnictvo.Uctovnictvo",
"komp_ac.uctovnictvo.Uctovnictvo",
"GetUctovnictvo",
),
);
@@ -250,13 +250,13 @@ pub mod uctovnictvo_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.uctovnictvo.Uctovnictvo/GetUctovnictvoCount",
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoCount",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.uctovnictvo.Uctovnictvo",
"komp_ac.uctovnictvo.Uctovnictvo",
"GetUctovnictvoCount",
),
);
@@ -279,13 +279,13 @@ pub mod uctovnictvo_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.uctovnictvo.Uctovnictvo/GetUctovnictvoByPosition",
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoByPosition",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.uctovnictvo.Uctovnictvo",
"komp_ac.uctovnictvo.Uctovnictvo",
"GetUctovnictvoByPosition",
),
);
@@ -308,13 +308,13 @@ pub mod uctovnictvo_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.uctovnictvo.Uctovnictvo/PutUctovnictvo",
"/komp_ac.uctovnictvo.Uctovnictvo/PutUctovnictvo",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.uctovnictvo.Uctovnictvo",
"komp_ac.uctovnictvo.Uctovnictvo",
"PutUctovnictvo",
),
);
@@ -447,7 +447,7 @@ pub mod uctovnictvo_server {
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/multieko2.uctovnictvo.Uctovnictvo/PostUctovnictvo" => {
"/komp_ac.uctovnictvo.Uctovnictvo/PostUctovnictvo" => {
#[allow(non_camel_case_types)]
struct PostUctovnictvoSvc<T: Uctovnictvo>(pub Arc<T>);
impl<
@@ -492,7 +492,7 @@ pub mod uctovnictvo_server {
};
Box::pin(fut)
}
"/multieko2.uctovnictvo.Uctovnictvo/GetUctovnictvo" => {
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvo" => {
#[allow(non_camel_case_types)]
struct GetUctovnictvoSvc<T: Uctovnictvo>(pub Arc<T>);
impl<
@@ -537,7 +537,7 @@ pub mod uctovnictvo_server {
};
Box::pin(fut)
}
"/multieko2.uctovnictvo.Uctovnictvo/GetUctovnictvoCount" => {
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoCount" => {
#[allow(non_camel_case_types)]
struct GetUctovnictvoCountSvc<T: Uctovnictvo>(pub Arc<T>);
impl<
@@ -583,7 +583,7 @@ pub mod uctovnictvo_server {
};
Box::pin(fut)
}
"/multieko2.uctovnictvo.Uctovnictvo/GetUctovnictvoByPosition" => {
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoByPosition" => {
#[allow(non_camel_case_types)]
struct GetUctovnictvoByPositionSvc<T: Uctovnictvo>(pub Arc<T>);
impl<
@@ -634,7 +634,7 @@ pub mod uctovnictvo_server {
};
Box::pin(fut)
}
"/multieko2.uctovnictvo.Uctovnictvo/PutUctovnictvo" => {
"/komp_ac.uctovnictvo.Uctovnictvo/PutUctovnictvo" => {
#[allow(non_camel_case_types)]
struct PutUctovnictvoSvc<T: Uctovnictvo>(pub Arc<T>);
impl<
@@ -714,7 +714,7 @@ pub mod uctovnictvo_server {
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "multieko2.uctovnictvo.Uctovnictvo";
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

@@ -71,6 +71,8 @@ feature-depth = 1
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = [
{ id = "RUSTSEC-2024-0014", reason = "generational-arena is archived but no safe upgrade path exists; accepted for now" },
{ id = "RUSTSEC-2024-0436", reason = "paste is archived; no immediate alternative in dependency tree; accepted for now" },
#"RUSTSEC-0000-0000",
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
@@ -100,6 +102,7 @@ allow = [
"HPND",
"ISC",
"LGPL-3.0",
"AGPL-3.0",
"MIT",
"MPL-2.0",
"NCSA",

View File

@@ -11,11 +11,11 @@ use tantivy::schema::{IndexRecordOption, Value};
use tantivy::{Index, TantivyDocument, Term};
use tonic::{Request, Response, Status};
use common::proto::multieko2::search::{
use common::proto::komp_ac::search::{
search_response::Hit, SearchRequest, SearchResponse,
};
pub use common::proto::multieko2::search::searcher_server::SearcherServer;
use common::proto::multieko2::search::searcher_server::Searcher;
pub use common::proto::komp_ac::search::searcher_server::SearcherServer;
use common::proto::komp_ac::search::searcher_server::Searcher;
use common::search::register_slovak_tokenizers;
use sqlx::{PgPool, Row};
use tracing::info;

View File

@@ -22,19 +22,23 @@ tonic = "0.13.0"
tonic-reflection = "0.13.0"
tracing = "0.1.41"
time = { version = "0.3.41", features = ["local-offset"] }
steel-derive = { git = "https://github.com/mattwparas/steel.git", branch = "master", package = "steel-derive" }
steel-core = { git = "https://github.com/mattwparas/steel.git", version = "0.6.0", features = ["anyhow", "dylibs", "sync"] }
thiserror = "2.0.12"
steel-derive = "0.6.0"
steel-core = { version = "0.7.0", features = ["anyhow", "dylibs", "sync"] }
dashmap = "6.1.0"
lazy_static = "1.5.0"
regex = "1.11.1"
bcrypt = "0.17.0"
validator = { version = "0.20.0", features = ["derive"] }
uuid = { version = "1.16.0", features = ["serde", "v4"] }
jsonwebtoken = "9.3.1"
rust-stemmers = "1.2.0"
rust_decimal = "1.37.2"
rust_decimal_macros = "1.37.1"
rust_decimal = { workspace = true }
rust_decimal_macros = { workspace = true }
regex = { workspace = true }
thiserror = { workspace = true }
steel-decimal = "1.0.0"
[lib]
name = "server"
@@ -46,3 +50,4 @@ rstest = "0.25.0"
lazy_static = "1.5.0"
rand = "0.9.1"
futures = "0.3.31"
tokio-test = "0.4.4"

View File

@@ -0,0 +1,44 @@
-- migrations/20250710201933_create_script_dependencies.sql
-- This table stores the dependency graph for table scripts
-- More efficient design with better indexing
CREATE TABLE script_dependencies (
id BIGSERIAL PRIMARY KEY,
-- The script that creates this dependency
script_id BIGINT NOT NULL REFERENCES table_scripts(id) ON DELETE CASCADE,
-- The table that depends on another (source of dependency)
source_table_id BIGINT NOT NULL REFERENCES table_definitions(id) ON DELETE CASCADE,
-- The table being depended upon (target of dependency)
target_table_id BIGINT NOT NULL REFERENCES table_definitions(id) ON DELETE CASCADE,
-- What type of dependency (for better debugging)
dependency_type TEXT NOT NULL CHECK (dependency_type IN ('column_access', 'sql_query', 'indexed_access')),
-- Additional context (column name, query snippet, etc.)
context_info JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Optimized indexes for fast cycle detection
CREATE INDEX idx_script_deps_source_target ON script_dependencies (source_table_id, target_table_id);
CREATE INDEX idx_script_deps_by_script ON script_dependencies (script_id);
CREATE INDEX idx_script_deps_by_target ON script_dependencies (target_table_id);
-- View for easy dependency analysis
CREATE VIEW dependency_graph AS
SELECT
sd.source_table_id,
sd.target_table_id,
st.table_name AS source_table,
tt.table_name AS target_table,
sd.dependency_type,
sd.context_info,
s.name AS schema_name
FROM script_dependencies sd
JOIN table_definitions st ON sd.source_table_id = st.id
JOIN table_definitions tt ON sd.target_table_id = tt.id
JOIN schemas s ON st.schema_id = s.id;

View File

@@ -1,7 +1,7 @@
grpcurl -plaintext -d '{
"username": "testuser2",
"email": "test2@example.com"
}' localhost:50051 multieko2.auth.AuthService/Register
}' localhost:50051 komp_ac.auth.AuthService/Register
{
"id": "5fa9bbce-85e0-4b06-8364-b561770c2fdd",
"username": "testuser2",

View File

@@ -3,7 +3,7 @@
"email": "test3@example.com",
"password": "your_password",
"password_confirmation": "your_password"
}' localhost:50051 multieko2.auth.AuthService/Register
}' localhost:50051 komp_ac.auth.AuthService/Register
{
"id": "96d2fd35-b39d-4c05-916a-66134453d34c",
"username": "testuser3",
@@ -12,14 +12,14 @@
}
grpcurl -plaintext -d '{
"identifier": "testuser3"
}' localhost:50051 multieko2.auth.AuthService/Login
}' localhost:50051 komp_ac.auth.AuthService/Login
ERROR:
Code: Unauthenticated
Message: Invalid credentials
grpcurl -plaintext -d '{
"identifier": "testuser3",
"password": "your_password"
}' localhost:50051 multieko2.auth.AuthService/Login
}' localhost:50051 komp_ac.auth.AuthService/Login
{
"accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI5NmQyZmQzNS1iMzlkLTRjMDUtOTE2YS02NjEzNDQ1M2QzNGMiLCJleHAiOjE3NDI5ODE2MTAsInJvbGUiOiJhY2NvdW50YW50In0.78VIR3X4QZohzeI5x3xmkmqcICTusOC6PELPohMV-k8",
"tokenType": "Bearer",
@@ -30,7 +30,7 @@ ERROR:
grpcurl -plaintext -d '{
"username": "testuser4",
"email": "test4@example.com"
}' localhost:50051 multieko2.auth.AuthService/Register
}' localhost:50051 komp_ac.auth.AuthService/Register
{
"id": "413d7ecc-f231-48af-8c5a-566b1dc2bf0b",
"username": "testuser4",
@@ -39,7 +39,7 @@ ERROR:
}
grpcurl -plaintext -d '{
"identifier": "test4@example.com"
}' localhost:50051 multieko2.auth.AuthService/Login
}' localhost:50051 komp_ac.auth.AuthService/Login
{
"accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0MTNkN2VjYy1mMjMxLTQ4YWYtOGM1YS01NjZiMWRjMmJmMGIiLCJleHAiOjE3NDI5ODE3MDEsInJvbGUiOiJhY2NvdW50YW50In0.4Hzu3tTZRNGHnBSgeCbGy2tFTl8EzpPdXBhcW8kuIc8",
"tokenType": "Bearer",
@@ -47,5 +47,5 @@ ERROR:
"userId": "413d7ecc-f231-48af-8c5a-566b1dc2bf0b",
"role": "accountant"
}
╭─    ~/Doc/pr/multieko2/server    auth ······ ✔
╭─    ~/Doc/pr/komp_ac/server    auth ······ ✔
╰─

View File

@@ -3,7 +3,7 @@
"email": "default@example.com",
"password": "password",
"password_confirmation": "password"
}' localhost:50051 multieko2.auth.AuthService/Register
}' localhost:50051 komp_ac.auth.AuthService/Register
{
"id": "73017288-af41-49d8-b4cb-2ac2109b38a1",
"username": "testuser_default_role",
@@ -16,7 +16,7 @@
"password": "password",
"password_confirmation": "password",
"role": "admin"
}' localhost:50051 multieko2.auth.AuthService/Register
}' localhost:50051 komp_ac.auth.AuthService/Register
{
"id": "9437e318-818b-4c34-822d-c2d6601b835e",
"username": "testuser_admin_role",
@@ -29,9 +29,9 @@
"password": "password",
"password_confirmation": "password",
"role": "invalid_role_name"
}' localhost:50051 multieko2.auth.AuthService/Register
}' localhost:50051 komp_ac.auth.AuthService/Register
ERROR:
Code: InvalidArgument
Message: Invalid role specified: 'invalid_role_name'
╭─    ~/Doc/pr/multieko2/server    main +3 !1 
╭─    ~/Doc/pr/komp_ac/server    main +3 !1 
╰─

View File

@@ -3,7 +3,7 @@ use bcrypt::verify;
use tonic::{Response, Status};
use crate::db::PgPool;
use crate::auth::logic::jwt; // Fixed import path
use common::proto::multieko2::auth::{LoginRequest, LoginResponse};
use common::proto::komp_ac::auth::{LoginRequest, LoginResponse};
pub async fn login(
pool: &PgPool,

View File

@@ -1,7 +1,7 @@
// src/auth/handlers/register.rs
use bcrypt::{hash, DEFAULT_COST};
use tonic::{Response, Status};
use common::proto::multieko2::auth::{RegisterRequest, AuthResponse};
use common::proto::komp_ac::auth::{RegisterRequest, AuthResponse};
use crate::db::PgPool;
use crate::auth::models::AuthError;

View File

@@ -4,7 +4,7 @@ use tonic_reflection::server::Builder as ReflectionBuilder;
use tokio::sync::mpsc;
use crate::indexer::{indexer_task, IndexCommand};
use common::proto::multieko2::FILE_DESCRIPTOR_SET;
use common::proto::komp_ac::FILE_DESCRIPTOR_SET;
use crate::server::services::{
TableStructureHandler,
TableDefinitionService,
@@ -12,7 +12,7 @@ use crate::server::services::{
TableScriptService,
AuthServiceImpl
};
use common::proto::multieko2::{
use common::proto::komp_ac::{
table_structure::table_structure_service_server::TableStructureServiceServer,
table_definition::table_definition_server::TableDefinitionServer,
tables_data::tables_data_server::TablesDataServer,

View File

@@ -1,6 +1,6 @@
// src/server/services/auth_service.rs
use tonic::{Request, Response, Status};
use common::proto::multieko2::auth::{
use common::proto::komp_ac::auth::{
auth_service_server::AuthService,
RegisterRequest, AuthResponse,
LoginRequest, LoginResponse

View File

@@ -1,6 +1,6 @@
// src/server/services/table_definition_service.rs
use tonic::{Request, Response, Status};
use common::proto::multieko2::{
use common::proto::komp_ac::{
common::Empty,
table_definition::{
table_definition_server::TableDefinition,

View File

@@ -1,6 +1,6 @@
// src/server/services/table_script_service.rs
use tonic::{Request, Response, Status};
use common::proto::multieko2::table_script::{
use common::proto::komp_ac::table_script::{
table_script_server::TableScript,
PostTableScriptRequest, TableScriptResponse
};

View File

@@ -1,8 +1,8 @@
// src/server/services/table_structure_service.rs
use tonic::{Request, Response, Status};
// Correct the import path for the TableStructureService trait
use common::proto::multieko2::table_structure::table_structure_service_server::TableStructureService;
use common::proto::multieko2::table_structure::{
use common::proto::komp_ac::table_structure::table_structure_service_server::TableStructureService;
use common::proto::komp_ac::table_structure::{
GetTableStructureRequest,
TableStructureResponse,
};

View File

@@ -5,9 +5,9 @@ use tonic::{Request, Response, Status};
use tokio::sync::mpsc;
use crate::indexer::IndexCommand;
use common::proto::multieko2::tables_data::tables_data_server::TablesData;
use common::proto::multieko2::common::CountResponse;
use common::proto::multieko2::tables_data::{
use common::proto::komp_ac::tables_data::tables_data_server::TablesData;
use common::proto::komp_ac::common::CountResponse;
use common::proto::komp_ac::tables_data::{
PostTableDataRequest, PostTableDataResponse,
PutTableDataRequest, PutTableDataResponse,
DeleteTableDataRequest, DeleteTableDataResponse,

View File

@@ -33,7 +33,7 @@ pub async fn qualify_table_name(
.unwrap_or(false);
if definition_exists {
Ok(format!("{}.\"{}\"", profile_name, table_name))
Ok(format!("\"{}\".\"{}\"", profile_name, table_name))
} else {
// It's not a user-defined table, so it must be a system table in 'public.
Ok(format!("\"{}\"", table_name))

View File

@@ -3,7 +3,7 @@
"target_column": "idea_name",
"script": "(begin (define idea_name \"aaa\") idea_name)",
"description": "Set idea_name to Awesome Idea"
}' localhost:50051 multieko2.table_script.TableScript/PostTableScript
}' localhost:50051 komp_ac.table_script.TableScript/PostTableScript
{
"id": "7"
}
@@ -17,7 +17,7 @@
"is_feasible": "true",
"last_updated": "2025-03-11T12:34:56Z"
}
}' localhost:50051 multieko2.tables_data.TablesData/PostTableData
}' localhost:50051 komp_ac.tables_data.TablesData/PostTableData
ERROR:
Code: InvalidArgument
Message: Validation failed for 'idea_name'. Expected: 'aaa', Received: 'sdfsfs'
@@ -31,7 +31,7 @@ ERROR:
"is_feasible": "true",
"last_updated": "2025-03-11T12:34:56Z"
}
}' localhost:50051 multieko2.tables_data.TablesData/PostTableData
}' localhost:50051 komp_ac.tables_data.TablesData/PostTableData
{
"success": true,
"message": "Data inserted successfully",

View File

@@ -6,7 +6,7 @@ Creation of the tables:
{"name": "yearly_goal", "field_type": "text"}
],
"profile_name": "finance"
}' localhost:50051 multieko2.table_definition.TableDefinition/PostTableDefinition
}' localhost:50051 komp_ac.table_definition.TableDefinition/PostTableDefinition
{
"success": true,
"sql": "CREATE TABLE \"2025_department\" (\n id BIGSERIAL PRIMARY KEY,\n deleted BOOLEAN NOT NULL DEFAULT FALSE,\n \"yearly_goal\" TEXT,\n created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP\n)\n"
@@ -22,7 +22,7 @@ Creation of the tables:
"links": [
{"linked_table_name": "2025_department", "required": true}
]
}' localhost:50051 multieko2.table_definition.TableDefinition/PostTableDefinition
}' localhost:50051 komp_ac.table_definition.TableDefinition/PostTableDefinition
{
"success": true,
"sql": "CREATE TABLE \"2025_project\" (\n id BIGSERIAL PRIMARY KEY,\n deleted BOOLEAN NOT NULL DEFAULT FALSE,\n \"department_id\" BIGINT NOT NULL REFERENCES \"2025_department\"(id),\n \"name\" TEXT,\n \"budget_estimate\" TEXT,\n created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP\n)\nCREATE INDEX idx_2025_project_department_fk ON \"2025_project\" (\"department_id\")\nCREATE INDEX idx_2025_project_name ON \"2025_project\" (\"name\")"

View File

@@ -1,19 +1,23 @@
// src/steel/server/execution.rs
use steel::steel_vm::engine::Engine;
use steel::steel_vm::register_fn::RegisterFn;
use steel::rvals::SteelVal;
use super::functions::SteelContext;
use super::functions::{SteelContext, convert_row_data_for_steel};
use steel_decimal::registry::FunctionRegistry;
use sqlx::PgPool;
use std::sync::Arc;
use std::collections::HashMap;
use thiserror::Error;
use tracing::{debug, error};
/// Represents different types of values that can be returned from Steel script execution.
#[derive(Debug)]
pub enum Value {
Strings(Vec<String>),
Numbers(Vec<i64>),
Mixed(Vec<SteelVal>),
}
/// Errors that can occur during Steel script execution.
#[derive(Debug, Error)]
pub enum ExecutionError {
#[error("Script execution failed: {0}")]
@@ -24,63 +28,162 @@ pub enum ExecutionError {
UnsupportedType(String),
}
pub fn execute_script(
/// Creates a Steel execution context with proper boolean value conversion.
pub async fn create_steel_context_with_boolean_conversion(
current_table: String,
schema_id: i64,
schema_name: String,
mut row_data: HashMap<String, String>,
db_pool: Arc<PgPool>,
) -> 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)
.await
.map_err(|e| {
error!("Failed to convert row data for Steel: {}", e);
ExecutionError::RuntimeError(format!("Failed to convert row data: {}", e))
})?;
Ok(SteelContext {
current_table,
schema_id,
schema_name,
row_data,
db_pool,
})
}
/// Executes a Steel script with database context and type-safe result processing.
pub async fn execute_script(
script: String,
target_type: &str,
_db_pool: Arc<PgPool>, // Passed to the SteelContext
context: SteelContext,
db_pool: Arc<PgPool>,
schema_id: i64,
schema_name: String,
current_table: String,
row_data: HashMap<String, String>,
) -> Result<Value, ExecutionError> {
let mut vm = Engine::new();
// Create execution context with proper boolean value conversion
let context = create_steel_context_with_boolean_conversion(
current_table,
schema_id,
schema_name,
row_data,
db_pool.clone(),
).await?;
let context = Arc::new(context);
// Register steel_get_column with row context
vm.register_fn("steel_get_column", {
let ctx = context.clone();
move |table: String, column: String| {
ctx.steel_get_column(&table, &column)
.map_err(|e| e.to_string())
}
});
// Register database access functions
register_steel_functions(&mut vm, context.clone());
// Register steel_get_column_with_index
vm.register_fn("steel_get_column_with_index", {
let ctx = context.clone();
move |table: String, index: i64, column: String| {
ctx.steel_get_column_with_index(&table, index, &column)
.map_err(|e| e.to_string())
}
});
// Register decimal math operations
register_decimal_math_functions(&mut vm);
// SQL query registration
vm.register_fn("steel_query_sql", {
let ctx = context.clone();
move |query: String| {
ctx.steel_query_sql(&query)
.map_err(|e| e.to_string())
}
});
// Register row data as variables in the Steel VM for get-var access
let mut define_script = String::new();
// Execute script and process results
let results = vm.compile_and_run_raw_program(script)
.map_err(|e| ExecutionError::RuntimeError(e.to_string()))?;
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));
}
// Convert results to target type
// Execute variable definitions if any exist
if !define_script.is_empty() {
vm.compile_and_run_raw_program(define_script)
.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());
// Execute the main script
let results = vm.compile_and_run_raw_program(script.clone())
.map_err(|e| {
error!("Steel script execution failed: {}", e);
error!("Script was: {}", script);
error!("Available variables were: {:?}", context.row_data);
ExecutionError::RuntimeError(e.to_string())
})?;
// Convert results to the requested target type
match target_type {
"STRINGS" => process_string_results(results),
_ => 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>) {
debug!("Registering Steel functions with context");
// Register column access function for current and related tables
vm.register_fn("steel_get_column", {
let ctx = context.clone();
move |table: String, column: String| {
debug!("steel_get_column called with table: '{}', column: '{}'", table, column);
ctx.steel_get_column(&table, &column)
.map_err(|e| {
error!("steel_get_column failed: {:?}", e);
e.to_string()
})
}
});
// Register indexed column access for comma-separated values
vm.register_fn("steel_get_column_with_index", {
let ctx = context.clone();
move |table: String, index: i64, column: String| {
debug!("steel_get_column_with_index called with table: '{}', index: {}, column: '{}'", table, index, column);
ctx.steel_get_column_with_index(&table, index, &column)
.map_err(|e| {
error!("steel_get_column_with_index failed: {:?}", e);
e.to_string()
})
}
});
// Register safe SQL query execution
vm.register_fn("steel_query_sql", {
let ctx = context.clone();
move |query: String| {
debug!("steel_query_sql called with query: '{}'", query);
ctx.steel_query_sql(&query)
.map_err(|e| {
error!("steel_query_sql failed: {:?}", e);
e.to_string()
})
}
});
}
/// Registers decimal mathematics functions in the Steel VM.
fn register_decimal_math_functions(vm: &mut Engine) {
debug!("Registering decimal math functions");
FunctionRegistry::register_all(vm);
}
/// Processes Steel script results into string format for consistent output.
fn process_string_results(results: Vec<SteelVal>) -> Result<Value, ExecutionError> {
let mut strings = Vec::new();
for result in results {
if let SteelVal::StringV(s) = result {
strings.push(s.to_string());
} else {
return Err(ExecutionError::TypeConversionError(
format!("Expected string, got {:?}", result)
));
}
let result_str = match result {
SteelVal::StringV(s) => s.to_string(),
SteelVal::NumV(n) => n.to_string(),
SteelVal::IntV(i) => i.to_string(),
SteelVal::BoolV(b) => b.to_string(),
_ => {
error!("Unexpected result type: {:?}", result);
return Err(ExecutionError::TypeConversionError(
format!("Expected string-convertible type, got {:?}", result)
));
}
};
strings.push(result_str);
}
Ok(Value::Strings(strings))
}

View File

@@ -1,4 +1,5 @@
// src/steel/server/functions.rs
use steel::rvals::SteelVal;
use sqlx::PgPool;
use std::collections::HashMap;
@@ -16,8 +17,14 @@ pub enum FunctionError {
TableNotFound(String),
#[error("Database error: {0}")]
DatabaseError(String),
#[error("Prohibited data type access: {0}")]
ProhibitedTypeAccess(String),
}
/// Data types that Steel scripts are prohibited from accessing for security reasons
const PROHIBITED_TYPES: &[&str] = &["BIGINT", "DATE", "TIMESTAMPTZ"];
/// Execution context for Steel scripts with database access capabilities.
#[derive(Clone)]
pub struct SteelContext {
pub current_table: String,
@@ -28,6 +35,8 @@ pub struct SteelContext {
}
impl SteelContext {
/// Resolves a base table name to its full qualified name in the current schema.
/// Used for foreign key relationship traversal in Steel scripts.
pub async fn get_related_table_name(&self, base_name: &str) -> Result<String, FunctionError> {
let table_def = sqlx::query!(
r#"SELECT table_name FROM table_definitions
@@ -43,13 +52,108 @@ impl SteelContext {
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!(
r#"SELECT columns FROM table_definitions
WHERE schema_id = $1 AND table_name = $2"#,
self.schema_id,
table_name
)
.fetch_optional(&*self.db_pool)
.await
.map_err(|e| FunctionError::DatabaseError(e.to_string()))?
.ok_or_else(|| FunctionError::TableNotFound(table_name.to_string()))?;
let columns: Vec<String> = serde_json::from_value(table_def.columns)
.map_err(|e| FunctionError::DatabaseError(format!("Invalid column data: {}", e)))?;
// Parse column definitions to find the requested column type
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_clean = name.trim_matches('"');
if column_name_clean == column_name {
return Ok(data_type.to_string());
}
}
}
Err(FunctionError::ColumnNotFound(format!(
"Column '{}' not found in table '{}'",
column_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 {
let normalized_type = normalize_data_type(column_type);
match normalized_type.as_str() {
"BOOLEAN" | "BOOL" => {
// Convert database boolean representations to Steel boolean syntax
match value.to_lowercase().as_str() {
"true" | "t" | "1" | "yes" | "on" => "#true".to_string(),
"false" | "f" | "0" | "no" | "off" => "#false".to_string(),
_ => value.to_string(), // Return as-is if not a recognized boolean
}
}
"INTEGER" => value.to_string(),
_ => value.to_string(), // Return as-is for other types
}
}
/// Validates that a column type is allowed for Steel script access.
/// Returns the column type if validation passes, error if prohibited.
async fn validate_column_type_and_get_type(&self, table_name: &str, column_name: &str) -> Result<String, FunctionError> {
let column_type = self.get_column_type(table_name, column_name).await?;
if is_prohibited_type(&column_type) {
return Err(FunctionError::ProhibitedTypeAccess(format!(
"Cannot access column '{}' in table '{}' because it has prohibited type '{}'. Steel scripts cannot access columns of type: {}",
column_name,
table_name,
column_type,
PROHIBITED_TYPES.join(", ")
)));
}
Ok(column_type)
}
/// Retrieves column value from current table or related tables via foreign keys.
///
/// # Behavior
/// - Current table: Returns value directly from row_data with type conversion
/// - Related table: Follows foreign key relationship and queries database
/// - 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 {
// Access current table data with type validation
let column_type = tokio::task::block_in_place(|| {
let handle = tokio::runtime::Handle::current();
handle.block_on(async {
self.validate_column_type_and_get_type(table, column).await
})
});
let column_type = match column_type {
Ok(ct) => ct,
Err(e) => return Err(SteelVal::StringV(e.to_string().into())),
};
return self.row_data.get(column)
.map(|v| SteelVal::StringV(v.clone().into()))
.map(|v| {
let converted_value = self.convert_value_to_steel_format(v, &column_type);
SteelVal::StringV(converted_value.into())
})
.ok_or_else(|| SteelVal::StringV(format!("Column {} not found", column).into()));
}
// Access related table via foreign key relationship
let base_name = table.split_once('_')
.map(|(_, rest)| rest)
.unwrap_or(table);
@@ -58,60 +162,103 @@ impl SteelContext {
let fk_value = self.row_data.get(&fk_column)
.ok_or_else(|| SteelVal::StringV(format!("Foreign key {} not found", fk_column).into()))?;
// Use `tokio::task::block_in_place` to safely block the thread
let result = tokio::task::block_in_place(|| {
let handle = tokio::runtime::Handle::current();
handle.block_on(async {
let actual_table = self.get_related_table_name(base_name).await
.map_err(|e| SteelVal::StringV(e.to_string().into()))?;
// Add quotes around the table name
sqlx::query_scalar::<_, String>(
// Validate column type and get type information
let column_type = self.validate_column_type_and_get_type(&actual_table, column).await
.map_err(|e| SteelVal::StringV(e.to_string().into()))?;
// Query the related table for the column value
let raw_value = sqlx::query_scalar::<_, String>(
&format!("SELECT {} FROM \"{}\".\"{}\" WHERE id = $1", column, self.schema_name, actual_table)
)
.bind(fk_value.parse::<i64>().map_err(|_|
SteelVal::StringV("Invalid foreign key format".into()))?)
.fetch_one(&*self.db_pool)
.await
.map_err(|e| SteelVal::StringV(e.to_string().into()))
.map_err(|e| SteelVal::StringV(e.to_string().into()))?;
// Convert to appropriate Steel format
let converted_value = self.convert_value_to_steel_format(&raw_value, &column_type);
Ok(converted_value)
})
});
result.map(|v| SteelVal::StringV(v.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(
&self,
table: &str,
index: i64,
column: &str
) -> Result<SteelVal, SteelVal> {
// Get the full value with proper type conversion
let value = self.steel_get_column(table, column)?;
if let SteelVal::StringV(s) = value {
let parts: Vec<_> = s.split(',').collect();
parts.get(index as usize)
.map(|v| SteelVal::StringV(v.trim().into()))
.ok_or_else(|| SteelVal::StringV("Index out of bounds".into()))
if let Some(part) = parts.get(index as usize) {
let trimmed_part = part.trim();
// Apply type conversion to the indexed part based on original column type
let column_type = tokio::task::block_in_place(|| {
let handle = tokio::runtime::Handle::current();
handle.block_on(async {
self.get_column_type(table, column).await
})
});
match column_type {
Ok(ct) => {
let converted_part = self.convert_value_to_steel_format(trimmed_part, &ct);
Ok(SteelVal::StringV(converted_part.into()))
}
Err(_) => {
// If type cannot be determined, return value as-is
Ok(SteelVal::StringV(trimmed_part.into()))
}
}
} else {
Err(SteelVal::StringV("Index out of bounds".into()))
}
} else {
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> {
// Validate query is read-only
if !is_read_only_query(query) {
return Err(SteelVal::StringV(
"Only SELECT queries are allowed".into()
));
}
if contains_prohibited_column_access(query) {
return Err(SteelVal::StringV(format!(
"SQL query may access prohibited column types. Steel scripts cannot access columns of type: {}",
PROHIBITED_TYPES.join(", ")
).into()));
}
let pool = self.db_pool.clone();
// Use `tokio::task::block_in_place` to safely block the thread
let result = tokio::task::block_in_place(|| {
let handle = tokio::runtime::Handle::current();
handle.block_on(async {
// Execute and get first column of all rows as strings
let rows = sqlx::query(query)
.fetch_all(&*pool)
.await
@@ -132,9 +279,85 @@ impl SteelContext {
}
}
/// Checks if a data type is prohibited for Steel script access.
fn is_prohibited_type(data_type: &str) -> bool {
let normalized_type = normalize_data_type(data_type);
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 {
data_type.to_uppercase()
.split('(') // Remove precision/scale from NUMERIC(x,y)
.next()
.unwrap_or(data_type)
.trim()
.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 {
let query_upper = query.to_uppercase();
let patterns = [
"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 {
let query = query.trim_start().to_uppercase();
query.starts_with("SELECT") ||
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

@@ -1,8 +1,6 @@
// src/steel/server/mod.rs
pub mod execution;
pub mod syntax_parser;
pub mod functions;
pub use execution::*;
pub use syntax_parser::*;
pub use functions::*;

View File

@@ -1,70 +0,0 @@
// src/steel/server/syntax_parser.rs
use regex::Regex;
use std::collections::HashSet;
pub struct SyntaxParser {
current_table_column_re: Regex,
different_table_column_re: Regex,
one_to_many_indexed_re: Regex,
sql_integration_re: Regex,
}
impl SyntaxParser {
pub fn new() -> Self {
SyntaxParser {
current_table_column_re: Regex::new(r"@(\w+)").unwrap(),
different_table_column_re: Regex::new(r"@(\w+)\.(\w+)").unwrap(),
one_to_many_indexed_re: Regex::new(r"@(\w+)\[(\d+)\]\.(\w+)").unwrap(),
sql_integration_re: Regex::new(r#"@sql\((['"])(.*?)['"]\)"#).unwrap(),
}
}
pub fn parse(&self, script: &str, current_table: &str) -> String {
let mut transformed = script.to_string();
// Process indexed access first to avoid overlap with relationship matches
transformed = self.one_to_many_indexed_re.replace_all(&transformed, |caps: &regex::Captures| {
format!("(steel_get_column_with_index \"{}\" {} \"{}\")",
&caps[1], &caps[2], &caps[3])
}).to_string();
// Process relationships
transformed = self.different_table_column_re.replace_all(&transformed, |caps: &regex::Captures| {
format!("(steel_get_column \"{}\" \"{}\")", &caps[1], &caps[2])
}).to_string();
// Process basic column access
transformed = self.current_table_column_re.replace_all(&transformed, |caps: &regex::Captures| {
format!("(steel_get_column \"{}\" \"{}\")", current_table, &caps[1])
}).to_string();
// Process SQL integration
transformed = self.sql_integration_re.replace_all(&transformed, |caps: &regex::Captures| {
format!("(steel_query_sql \"{}\")", &caps[2])
}).to_string();
transformed
}
pub fn extract_dependencies(&self, script: &str, current_table: &str) -> (HashSet<String>, HashSet<String>) {
let mut tables = HashSet::new();
let mut columns = HashSet::new();
for cap in self.current_table_column_re.captures_iter(script) {
tables.insert(current_table.to_string());
columns.insert(cap[1].to_string());
}
for cap in self.different_table_column_re.captures_iter(script) {
tables.insert(cap[1].to_string());
columns.insert(cap[2].to_string());
}
for cap in self.one_to_many_indexed_re.captures_iter(script) {
tables.insert(cap[1].to_string());
columns.insert(cap[3].to_string());
}
(tables, columns)
}
}

View File

@@ -15,7 +15,7 @@
"indexes": ["company_name", "is_active"],
"profile_name": "default",
"linked_table_name": ""
}' localhost:50051 multieko2.table_definition.TableDefinition/PostTableDefinition
}' localhost:50051 komp_ac.table_definition.TableDefinition/PostTableDefinition
ERROR:
Code: NotFound
Message: Linked table not found in profile
@@ -35,12 +35,12 @@ ERROR:
],
"indexes": ["company_name", "is_active"],
"profile_name": "default"
}' localhost:50051 multieko2.table_definition.TableDefinition/PostTableDefinition
}' localhost:50051 komp_ac.table_definition.TableDefinition/PostTableDefinition
{
"success": true,
"sql": "CREATE TABLE \"2025_company_data1\" (\n id BIGSERIAL PRIMARY KEY,\n deleted BOOLEAN NOT NULL DEFAULT FALSE,\n firma TEXT NOT NULL,\n \"company_name\" TEXT,\n \"textfield\" TEXT,\n \"textfield2\" TEXT,\n \"textfield3\" TEXT,\n \"headquarters_psc\" TEXT,\n \"contact_phone\" VARCHAR(15),\n \"office_address\" TEXT,\n \"support_email\" VARCHAR(255),\n \"is_active\" BOOLEAN,\n \"last_updated\" TIMESTAMPTZ,\n created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP\n)\nCREATE INDEX idx_2025_company_data1_firma ON \"2025_company_data1\" (firma)\nCREATE INDEX idx_2025_company_data1_company_name ON \"2025_company_data1\" (\"company_name\")\nCREATE INDEX idx_2025_company_data1_is_active ON \"2025_company_data1\" (\"is_active\")"
}
grpcurl -plaintext -d '{}' localhost:50051 multieko2.table_definition.TableDefinition/GetProfileTree
grpcurl -plaintext -d '{}' localhost:50051 komp_ac.table_definition.TableDefinition/GetProfileTree
{
"profiles": [
{
@@ -56,12 +56,12 @@ ERROR:
grpcurl -plaintext -d '{
"profile_name": "default",
"table_name": "2025_company_data1"
}' localhost:50051 multieko2.table_definition.TableDefinition/DeleteTable
}' localhost:50051 komp_ac.table_definition.TableDefinition/DeleteTable
{
"success": true,
"message": "Table '2025_company_data1' and its definition were successfully removed"
}
grpcurl -plaintext -d '{}' localhost:50051 multieko2.table_definition.TableDefinition/GetProfileTree
grpcurl -plaintext -d '{}' localhost:50051 komp_ac.table_definition.TableDefinition/GetProfileTree
{}
╭─    ~/Doc/pr/multieko2/server    main ⇡1 ········· ✔
╭─    ~/Doc/pr/komp_ac/server    main ⇡1 ········· ✔
╰─

View File

@@ -1,4 +1,4 @@
grpcurl -plaintext -d '{}' localhost:50051 multieko2.table_definition.TableDefinition/GetProfileTree
grpcurl -plaintext -d '{}' localhost:50051 komp_ac.table_definition.TableDefinition/GetProfileTree
{
"profiles": [
{
@@ -80,12 +80,12 @@
"indexes": ["column1", "columnd"],
"profile_name": "new_profile",
"linked_table_name": "2025_multi_dependent_table3"
}' localhost:50051 multieko2.table_definition.TableDefinition/PostTableDefinition
}' localhost:50051 komp_ac.table_definition.TableDefinition/PostTableDefinition
{
"success": true,
"sql": "CREATE TABLE \"2025_multi_dependent_table5\" (\n id BIGSERIAL PRIMARY KEY,\n deleted BOOLEAN NOT NULL DEFAULT FALSE,\n firma TEXT NOT NULL,\n \"multi_dependent_table3_id\" BIGINT NOT NULL REFERENCES \"2025_multi_dependent_table3\"(id),\n \"2025_column1\" TEXT,\n \"2025_columnx\" TEXT,\n \"2025_columny\" TEXT,\n \"2025_columnz\" TEXT,\n \"2025_columna\" TEXT,\n \"2025_columnb\" TEXT,\n \"2025_columnc\" TEXT,\n \"2025_columnd\" TEXT,\n \"2025_column2\" INTEGER,\n created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP\n)\nCREATE INDEX idx_2025_multi_dependent_table5_firma ON \"2025_multi_dependent_table5\" (firma)\nCREATE INDEX idx_2025_multi_dependent_table5_multi_dependent_table3_id ON \"2025_multi_dependent_table5\" (\"multi_dependent_table3_id\")\nCREATE INDEX idx_2025_multi_dependent_table5_2025_column1 ON \"2025_multi_dependent_table5\" (\"2025_column1\")\nCREATE INDEX idx_2025_multi_dependent_table5_2025_columnd ON \"2025_multi_dependent_table5\" (\"2025_columnd\")"
}
grpcurl -plaintext -d '{}' localhost:50051 multieko2.table_definition.TableDefinition/GetProfileTree
grpcurl -plaintext -d '{}' localhost:50051 komp_ac.table_definition.TableDefinition/GetProfileTree
{
"profiles": [
{

View File

@@ -1,4 +1,4 @@
grpcurl -plaintext -d '{}' localhost:50051 multieko2.table_definition.TableDefinition/GetProfileTree
grpcurl -plaintext -d '{}' localhost:50051 komp_ac.table_definition.TableDefinition/GetProfileTree
{
"profiles": [
{
@@ -44,12 +44,12 @@
],
"indexes": ["firma", "mesto"],
"profile_name": "default"
}' localhost:50051 multieko2.table_definition.TableDefinition/PostTableDefinition
}' localhost:50051 komp_ac.table_definition.TableDefinition/PostTableDefinition
{
"success": true,
"sql": "CREATE TABLE \"2025_adresar6\" (\n id BIGSERIAL PRIMARY KEY,\n deleted BOOLEAN NOT NULL DEFAULT FALSE,\n \"firma\" TEXT,\n \"kz\" TEXT,\n \"drc\" TEXT,\n \"ulica\" TEXT,\n \"psc\" TEXT,\n \"mesto\" TEXT,\n \"stat\" TEXT,\n \"banka\" TEXT,\n \"ucet\" TEXT,\n \"skladm\" TEXT,\n \"ico\" TEXT,\n \"kontakt\" TEXT,\n \"telefon\" VARCHAR(15),\n \"skladu\" TEXT,\n \"fax\" TEXT,\n created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP\n)\nCREATE INDEX idx_2025_adresar6_firma ON \"2025_adresar6\" (\"firma\")\nCREATE INDEX idx_2025_adresar6_mesto ON \"2025_adresar6\" (\"mesto\")"
}
grpcurl -plaintext -d '{}' localhost:50051 multieko2.table_definition.TableDefinition/GetProfileTree
grpcurl -plaintext -d '{}' localhost:50051 komp_ac.table_definition.TableDefinition/GetProfileTree
{
"profiles": [
{
@@ -154,4 +154,4 @@ grpcurl -plaintext -d '{
{"linked_table_name": "2025_shipping_provider", "required": false},
{"linked_table_name": "2025_ecom_product", "required": false}
]
}' localhost:50051 multieko2.table_definition.TableDefinition/PostTableDefinition
}' localhost:50051 komp_ac.table_definition.TableDefinition/PostTableDefinition

View File

@@ -1,7 +1,7 @@
// src/table_definition/handlers/delete_table.rs
use tonic::Status;
use sqlx::PgPool;
use common::proto::multieko2::table_definition::{DeleteTableRequest, DeleteTableResponse};
use common::proto::komp_ac::table_definition::{DeleteTableRequest, DeleteTableResponse};
pub async fn delete_table(
db_pool: &PgPool,

View File

@@ -1,7 +1,7 @@
// src/table_definition/handlers/get_profile_tree.rs
use tonic::{Request, Response, Status};
use sqlx::PgPool;
use common::proto::multieko2::{
use common::proto::komp_ac::{
common::Empty,
table_definition::{
ProfileTreeResponse,

View File

@@ -3,7 +3,7 @@
use tonic::Status;
use sqlx::{PgPool, Transaction, Postgres};
use serde_json::json;
use common::proto::multieko2::table_definition::{PostTableDefinitionRequest, TableDefinitionResponse};
use common::proto::komp_ac::table_definition::{PostTableDefinitionRequest, TableDefinitionResponse};
const PREDEFINED_FIELD_TYPES: &[(&str, &str)] = &[
("text", "TEXT"),

View File

@@ -4,7 +4,7 @@ This is how we can push script into the database, now its stored after a push in
"target_column": "fax",
"script": "(set! fax telefon)",
"description": "Copy telefon value to fax column"
}' localhost:50051 multieko2.table_script.TableScript/PostTableScript
}' localhost:50051 komp_ac.table_script.TableScript/PostTableScript
{
"id": "2"
}

View File

@@ -1,4 +1,8 @@
// src/table_script/handlers.rs
pub mod dependency_analyzer;
pub mod dependency_utils;
pub mod post_table_script;
pub use post_table_script::post_table_script;
pub use dependency_analyzer::{DependencyAnalyzer, DependencyError, Dependency, DependencyType};
pub use dependency_utils::*;
pub use post_table_script::*;

View File

@@ -0,0 +1,566 @@
// src/table_script/handlers/dependency_analyzer.rs
use std::collections::HashMap;
use tonic::Status;
use serde_json::{json, Value};
/// Represents the state of a node during dependency graph traversal.
#[derive(Clone, Copy, PartialEq)]
enum NodeState {
Unvisited,
Visiting, // Currently in recursion stack
Visited, // Completely processed
}
/// Represents a dependency relationship between tables in Steel scripts.
#[derive(Debug, Clone)]
pub struct Dependency {
pub target_table: String,
pub dependency_type: DependencyType,
pub context: Option<Value>,
}
/// Types of dependencies that can exist between tables in Steel scripts.
#[derive(Debug, Clone)]
pub enum DependencyType {
/// Direct column access via steel_get_column
ColumnAccess { column: String },
/// Indexed column access via steel_get_column_with_index
IndexedAccess { column: String, index: i64 },
/// Raw SQL query access via steel_query_sql
SqlQuery { query_fragment: String },
}
impl DependencyType {
/// Returns the string representation used in the database.
pub fn as_str(&self) -> &'static str {
match self {
DependencyType::ColumnAccess { .. } => "column_access",
DependencyType::IndexedAccess { .. } => "indexed_access",
DependencyType::SqlQuery { .. } => "sql_query",
}
}
/// Generates context JSON for database storage.
pub fn context_json(&self) -> Value {
match self {
DependencyType::ColumnAccess { column } => {
json!({ "column": column })
}
DependencyType::IndexedAccess { column, index } => {
json!({ "column": column, "index": index })
}
DependencyType::SqlQuery { query_fragment } => {
json!({ "query_fragment": query_fragment })
}
}
}
}
/// Errors that can occur during dependency analysis.
#[derive(Debug)]
pub enum DependencyError {
CircularDependency {
cycle_path: Vec<String>,
involving_script: String
},
InvalidTableReference {
table_name: String,
script_context: String
},
ScriptParseError {
error: String
},
DatabaseError {
error: String
},
}
impl From<DependencyError> for Status {
fn from(error: DependencyError) -> Self {
match error {
DependencyError::CircularDependency { cycle_path, involving_script } => {
Status::failed_precondition(format!(
"Circular dependency detected in script for '{}': {}",
involving_script,
cycle_path.join(" -> ")
))
}
DependencyError::InvalidTableReference { table_name, script_context } => {
Status::not_found(format!(
"Table '{}' referenced in script '{}' does not exist",
table_name, script_context
))
}
DependencyError::ScriptParseError { error } => {
Status::invalid_argument(format!("Script parsing failed: {}", error))
}
DependencyError::DatabaseError { error } => {
Status::internal(format!("Database error: {}", error))
}
}
}
}
/// Analyzes Steel scripts to extract table dependencies and validate referential integrity.
///
/// This analyzer identifies how tables reference each other through Steel function calls
/// and ensures that dependency graphs remain acyclic while respecting table link constraints.
pub struct DependencyAnalyzer {
schema_id: i64,
}
impl DependencyAnalyzer {
/// Creates a new dependency analyzer for the specified schema.
pub fn new(schema_id: i64) -> Self {
Self { schema_id }
}
/// Analyzes a Steel script to extract all table dependencies.
///
/// Uses regex patterns to identify function calls that create dependencies:
/// - `steel_get_column` calls for direct column access
/// - `steel_get_column_with_index` calls for indexed access
/// - `steel_query_sql` calls for raw SQL access
/// - `get-var` calls in transformed scripts
///
/// # Arguments
/// * `script` - The Steel script code to analyze
/// * `current_table_name` - Name of the table this script belongs to (for self-references)
///
/// # Returns
/// * `Ok(Vec<Dependency>)` - List of identified dependencies
/// * `Err(DependencyError)` - If script parsing fails
pub fn analyze_script_dependencies(&self, script: &str, current_table_name: &str) -> Result<Vec<Dependency>, DependencyError> {
let mut dependencies = Vec::new();
// Extract different types of dependencies using regex patterns
dependencies.extend(self.extract_function_calls(script)?);
dependencies.extend(self.extract_sql_dependencies(script)?);
dependencies.extend(self.extract_get_var_calls(script, current_table_name)?);
Ok(dependencies)
}
/// Extracts Steel function calls that create table dependencies.
fn extract_function_calls(&self, script: &str) -> Result<Vec<Dependency>, DependencyError> {
let mut dependencies = Vec::new();
// Pattern: (steel_get_column "table" "column")
let column_pattern = regex::Regex::new(r#"\(\s*steel_get_column\s+"([^"]+)"\s+"([^"]+)""#)
.map_err(|e: regex::Error| DependencyError::ScriptParseError { error: e.to_string() })?;
for caps in column_pattern.captures_iter(script) {
let table = caps[1].to_string();
let column = caps[2].to_string();
dependencies.push(Dependency {
target_table: table,
dependency_type: DependencyType::ColumnAccess { column },
context: None,
});
}
// Pattern: (steel_get_column_with_index "table" index "column")
let indexed_pattern = regex::Regex::new(r#"\(\s*steel_get_column_with_index\s+"([^"]+)"\s+(\d+)\s+"([^"]+)""#)
.map_err(|e: regex::Error| DependencyError::ScriptParseError { error: e.to_string() })?;
for caps in indexed_pattern.captures_iter(script) {
let table = caps[1].to_string();
let index: i64 = caps[2].parse()
.map_err(|e: std::num::ParseIntError| DependencyError::ScriptParseError { error: e.to_string() })?;
let column = caps[3].to_string();
dependencies.push(Dependency {
target_table: table,
dependency_type: DependencyType::IndexedAccess { column, index },
context: None,
});
}
Ok(dependencies)
}
/// Extracts get-var calls as dependencies for transformed scripts.
/// These represent self-references to the current table.
fn extract_get_var_calls(&self, script: &str, current_table_name: &str) -> Result<Vec<Dependency>, DependencyError> {
let mut dependencies = Vec::new();
// Pattern: (get-var "variable")
let get_var_pattern = regex::Regex::new(r#"\(get-var\s+"([^"]+)"\)"#)
.map_err(|e: regex::Error| DependencyError::ScriptParseError { error: e.to_string() })?;
for caps in get_var_pattern.captures_iter(script) {
let variable_name = caps[1].to_string();
dependencies.push(Dependency {
target_table: current_table_name.to_string(),
dependency_type: DependencyType::ColumnAccess { column: variable_name },
context: None,
});
}
Ok(dependencies)
}
/// Extracts table references from SQL queries in steel_query_sql calls.
fn extract_sql_dependencies(&self, script: &str) -> Result<Vec<Dependency>, DependencyError> {
let mut dependencies = Vec::new();
// Pattern: (steel_query_sql "SELECT ... FROM table ...")
let sql_pattern = regex::Regex::new(r#"\(\s*steel_query_sql\s+"([^"]+)""#)
.map_err(|e: regex::Error| DependencyError::ScriptParseError { error: e.to_string() })?;
for caps in sql_pattern.captures_iter(script) {
let query = caps[1].to_string();
let table_refs = self.extract_table_references_from_sql(&query)?;
for table in table_refs {
dependencies.push(Dependency {
target_table: table.clone(),
dependency_type: DependencyType::SqlQuery {
query_fragment: query.clone()
},
context: None,
});
}
}
Ok(dependencies)
}
/// Extracts table names from SQL query text using regex patterns.
/// Looks for FROM and JOIN clauses to identify table references.
fn extract_table_references_from_sql(&self, sql: &str) -> Result<Vec<String>, DependencyError> {
let mut tables = Vec::new();
// Pattern: FROM table_name or JOIN table_name
let table_pattern = regex::Regex::new(r#"(?i)\b(?:FROM|JOIN)\s+(?:"([^"]+)"|(\w+))"#)
.map_err(|e: regex::Error| DependencyError::ScriptParseError { error: e.to_string() })?;
for caps in table_pattern.captures_iter(sql) {
let table = caps.get(1)
.or_else(|| caps.get(2))
.map(|m| m.as_str().to_string());
if let Some(table_name) = table {
tables.push(table_name);
}
}
Ok(tables)
}
/// Checks for circular dependencies in the dependency graph.
///
/// This function validates that adding new dependencies won't create cycles
/// that could lead to infinite loops during script execution. Self-references
/// are explicitly allowed and filtered out from cycle detection.
///
/// # Arguments
/// * `tx` - Database transaction for querying existing dependencies
/// * `table_id` - ID of the table adding new dependencies
/// * `new_dependencies` - Dependencies to be added
///
/// # Returns
/// * `Ok(())` - No cycles detected, safe to add dependencies
/// * `Err(DependencyError)` - Cycle detected or validation failed
pub async fn check_for_cycles(
&self,
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
table_id: i64,
new_dependencies: &[Dependency],
) -> Result<(), DependencyError> {
// Validate that structured table access respects link constraints
self.validate_link_constraints(tx, table_id, new_dependencies).await?;
// Build current dependency graph excluding self-references
let current_deps = sqlx::query!(
r#"SELECT sd.source_table_id, sd.target_table_id, st.table_name as source_name, tt.table_name as target_name
FROM script_dependencies sd
JOIN table_definitions st ON sd.source_table_id = st.id
JOIN table_definitions tt ON sd.target_table_id = tt.id
WHERE st.schema_id = $1"#,
self.schema_id
)
.fetch_all(&mut **tx)
.await
.map_err(|e| DependencyError::DatabaseError { error: e.to_string() })?;
let mut graph: HashMap<i64, Vec<i64>> = HashMap::new();
let mut table_names: HashMap<i64, String> = HashMap::new();
// Build adjacency list excluding self-references
for dep in current_deps {
if dep.source_table_id != dep.target_table_id {
graph.entry(dep.source_table_id).or_default().push(dep.target_table_id);
}
table_names.insert(dep.source_table_id, dep.source_name);
table_names.insert(dep.target_table_id, dep.target_name);
}
// Add new dependencies to test (excluding self-references)
for dep in new_dependencies {
let target_id = sqlx::query_scalar!(
"SELECT id FROM table_definitions WHERE schema_id = $1 AND table_name = $2",
self.schema_id,
dep.target_table
)
.fetch_optional(&mut **tx)
.await
.map_err(|e| DependencyError::DatabaseError { error: e.to_string() })?
.ok_or_else(|| DependencyError::InvalidTableReference {
table_name: dep.target_table.clone(),
script_context: format!("table_id_{}", table_id),
})?;
// Only add to cycle detection if not a self-reference
if table_id != target_id {
graph.entry(table_id).or_default().push(target_id);
}
// Ensure table names are available for error reporting
if !table_names.contains_key(&table_id) {
let source_name = sqlx::query_scalar!(
"SELECT table_name FROM table_definitions WHERE id = $1",
table_id
)
.fetch_one(&mut **tx)
.await
.map_err(|e| DependencyError::DatabaseError { error: e.to_string() })?;
table_names.insert(table_id, source_name);
}
}
// Detect cycles using DFS algorithm
self.detect_cycles_dfs(&graph, &table_names, table_id)?;
Ok(())
}
/// Performs depth-first search to detect cycles in the dependency graph.
fn dfs_visit(
&self,
node: i64,
states: &mut HashMap<i64, NodeState>,
graph: &HashMap<i64, Vec<i64>>,
path: &mut Vec<i64>,
table_names: &HashMap<i64, String>,
starting_table: i64,
) -> Result<(), DependencyError> {
states.insert(node, NodeState::Visiting);
path.push(node);
if let Some(neighbors) = graph.get(&node) {
for &neighbor in neighbors {
if !states.contains_key(&neighbor) {
states.insert(neighbor, NodeState::Unvisited);
}
match states.get(&neighbor).copied().unwrap_or(NodeState::Unvisited) {
NodeState::Visiting => {
// Skip self-references as they're allowed
if neighbor == node {
continue;
}
// Found a cycle - build the cycle path
let cycle_start_idx = path.iter().position(|&x| x == neighbor).unwrap_or(0);
let cycle_path: Vec<String> = path[cycle_start_idx..]
.iter()
.chain(std::iter::once(&neighbor))
.map(|&id| table_names.get(&id).cloned().unwrap_or_else(|| id.to_string()))
.collect();
// Only report as error if cycle involves multiple tables
if cycle_path.len() > 2 || (cycle_path.len() == 2 && cycle_path[0] != cycle_path[1]) {
let involving_script = table_names.get(&starting_table)
.cloned()
.unwrap_or_else(|| starting_table.to_string());
return Err(DependencyError::CircularDependency {
cycle_path,
involving_script,
});
}
}
NodeState::Unvisited => {
self.dfs_visit(neighbor, states, graph, path, table_names, starting_table)?;
}
NodeState::Visited => {
// Already processed, no cycle through this path
}
}
}
}
path.pop();
states.insert(node, NodeState::Visited);
Ok(())
}
/// Validates that structured table access respects table link constraints.
///
/// # Access Rules
/// - Self-references are always allowed (table can access its own columns)
/// - Structured access (steel_get_column functions) requires explicit table links
/// - Raw SQL access (steel_query_sql) is unrestricted
///
/// # Arguments
/// * `tx` - Database transaction for querying table links
/// * `source_table_id` - ID of the table with the script
/// * `dependencies` - Dependencies to validate
async fn validate_link_constraints(
&self,
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
source_table_id: i64,
dependencies: &[Dependency],
) -> Result<(), DependencyError> {
let current_table_name = sqlx::query_scalar!(
"SELECT table_name FROM table_definitions WHERE id = $1",
source_table_id
)
.fetch_one(&mut **tx)
.await
.map_err(|e| DependencyError::DatabaseError { error: e.to_string() })?;
// Get all valid linked tables for the source table
let linked_tables = sqlx::query!(
r#"SELECT td.table_name, tdl.is_required
FROM table_definition_links tdl
JOIN table_definitions td ON tdl.linked_table_id = td.id
WHERE tdl.source_table_id = $1"#,
source_table_id
)
.fetch_all(&mut **tx)
.await
.map_err(|e| DependencyError::DatabaseError { error: e.to_string() })?;
let mut allowed_tables: std::collections::HashSet<String> = linked_tables
.into_iter()
.map(|row| row.table_name)
.collect();
// Self-references are always allowed
allowed_tables.insert(current_table_name.clone());
// Validate each dependency
for dep in dependencies {
match &dep.dependency_type {
DependencyType::ColumnAccess { column } | DependencyType::IndexedAccess { column, .. } => {
// Allow self-references
if dep.target_table == current_table_name {
continue;
}
// Check if table is linked
if !allowed_tables.contains(&dep.target_table) {
return Err(DependencyError::InvalidTableReference {
table_name: dep.target_table.clone(),
script_context: format!(
"Table '{}' is not linked to '{}'. Add a link in the table definition to access '{}' via steel_get_column functions. Column attempted: '{}'. Note: Self-references are always allowed.",
dep.target_table,
current_table_name,
dep.target_table,
column
),
});
}
}
DependencyType::SqlQuery { .. } => {
// Raw SQL access is unrestricted
}
}
}
Ok(())
}
/// Runs DFS-based cycle detection on the dependency graph.
fn detect_cycles_dfs(
&self,
graph: &HashMap<i64, Vec<i64>>,
table_names: &HashMap<i64, String>,
starting_table: i64,
) -> Result<(), DependencyError> {
let mut states: HashMap<i64, NodeState> = HashMap::new();
// Initialize all nodes as unvisited
for &node in graph.keys() {
states.insert(node, NodeState::Unvisited);
}
// Run DFS from each unvisited node
for &node in graph.keys() {
if states.get(&node) == Some(&NodeState::Unvisited) {
let mut path = Vec::new();
if let Err(cycle_error) = self.dfs_visit(
node,
&mut states,
graph,
&mut path,
table_names,
starting_table
) {
return Err(cycle_error);
}
}
}
Ok(())
}
/// Saves dependencies to the database within an existing transaction.
///
/// This function replaces all existing dependencies for a script with the new set,
/// ensuring the database reflects the current script analysis results.
///
/// # Arguments
/// * `tx` - Database transaction for atomic updates
/// * `script_id` - ID of the script these dependencies belong to
/// * `table_id` - ID of the table containing the script
/// * `dependencies` - Dependencies to save
pub async fn save_dependencies(
&self,
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
script_id: i64,
table_id: i64,
dependencies: &[Dependency],
) -> Result<(), DependencyError> {
// Clear existing dependencies for this script
sqlx::query!("DELETE FROM script_dependencies WHERE script_id = $1", script_id)
.execute(&mut **tx)
.await
.map_err(|e| DependencyError::DatabaseError { error: e.to_string() })?;
// Insert new dependencies
for dep in dependencies {
let target_id = sqlx::query_scalar!(
"SELECT id FROM table_definitions WHERE schema_id = $1 AND table_name = $2",
self.schema_id,
dep.target_table
)
.fetch_optional(&mut **tx)
.await
.map_err(|e| DependencyError::DatabaseError { error: e.to_string() })?
.ok_or_else(|| DependencyError::InvalidTableReference {
table_name: dep.target_table.clone(),
script_context: format!("script_id_{}", script_id),
})?;
sqlx::query!(
r#"INSERT INTO script_dependencies
(script_id, source_table_id, target_table_id, dependency_type, context_info)
VALUES ($1, $2, $3, $4, $5)"#,
script_id,
table_id,
target_id,
dep.dependency_type.as_str(),
dep.dependency_type.context_json()
)
.execute(&mut **tx)
.await
.map_err(|e| DependencyError::DatabaseError { error: e.to_string() })?;
}
Ok(())
}
}

View File

@@ -0,0 +1,229 @@
// src/table_script/handlers/dependency_utils.rs
// Utility functions for dependency analysis and debugging
use sqlx::PgPool;
use serde_json::{json, Value};
use std::collections::HashMap;
/// Get a visual representation of the dependency graph for a schema
pub async fn get_dependency_graph_visualization(
db_pool: &PgPool,
schema_id: i64,
) -> Result<Value, sqlx::Error> {
let dependencies = sqlx::query!(
r#"SELECT
st.table_name as source_table,
tt.table_name as target_table,
sd.dependency_type,
sd.context_info,
COUNT(*) as dependency_count
FROM script_dependencies sd
JOIN table_definitions st ON sd.source_table_id = st.id
JOIN table_definitions tt ON sd.target_table_id = tt.id
WHERE st.schema_id = $1
GROUP BY st.table_name, tt.table_name, sd.dependency_type, sd.context_info
ORDER BY st.table_name, tt.table_name"#,
schema_id
)
.fetch_all(db_pool)
.await?;
let mut graph = HashMap::new();
for dep in dependencies {
let source_entry = graph.entry(dep.source_table.clone()).or_insert_with(|| json!({
"table": dep.source_table,
"dependencies": []
}));
if let Some(deps_array) = source_entry.get_mut("dependencies") {
if let Some(deps) = deps_array.as_array_mut() {
deps.push(json!({
"target": dep.target_table,
"type": dep.dependency_type,
"context": dep.context_info,
"count": dep.dependency_count.unwrap_or(0)
}));
}
}
}
Ok(json!({
"schema_id": schema_id,
"dependency_graph": graph.into_values().collect::<Vec<_>>()
}))
}
/// Check if a schema has any circular dependencies
pub async fn check_schema_for_cycles(
db_pool: &PgPool,
schema_id: i64,
) -> Result<Vec<String>, sqlx::Error> {
// This is a simplified cycle detection for monitoring purposes
let dependencies = sqlx::query!(
r#"SELECT sd.source_table_id, sd.target_table_id, st.table_name as source_table, tt.table_name as target_table
FROM script_dependencies sd
JOIN table_definitions st ON sd.source_table_id = st.id
JOIN table_definitions tt ON sd.target_table_id = tt.id
WHERE st.schema_id = $1"#,
schema_id
)
.fetch_all(db_pool)
.await?;
let mut graph: HashMap<i64, Vec<i64>> = HashMap::new();
let mut table_names: HashMap<i64, String> = HashMap::new();
for dep in dependencies {
graph.entry(dep.source_table_id).or_default().push(dep.target_table_id);
table_names.insert(dep.source_table_id, dep.source_table);
table_names.insert(dep.target_table_id, dep.target_table);
}
// Simple cycle detection using DFS
let mut visited = std::collections::HashSet::new();
let mut rec_stack = std::collections::HashSet::new();
let mut cycles = Vec::new();
for &node in graph.keys() {
if !visited.contains(&node) {
if let Some(cycle) = detect_cycle_dfs(
node,
&graph,
&mut visited,
&mut rec_stack,
&table_names
) {
cycles.push(cycle);
}
}
}
Ok(cycles)
}
fn detect_cycle_dfs(
node: i64,
graph: &HashMap<i64, Vec<i64>>,
visited: &mut std::collections::HashSet<i64>,
rec_stack: &mut std::collections::HashSet<i64>,
table_names: &HashMap<i64, String>,
) -> Option<String> {
visited.insert(node);
rec_stack.insert(node);
if let Some(neighbors) = graph.get(&node) {
for &neighbor in neighbors {
if !visited.contains(&neighbor) {
if let Some(cycle) = detect_cycle_dfs(neighbor, graph, visited, rec_stack, table_names) {
return Some(cycle);
}
} else if rec_stack.contains(&neighbor) {
// Found a cycle
let node_name = table_names.get(&node).cloned().unwrap_or_else(|| node.to_string());
let neighbor_name = table_names.get(&neighbor).cloned().unwrap_or_else(|| neighbor.to_string());
return Some(format!("{} -> {}", node_name, neighbor_name));
}
}
}
rec_stack.remove(&node);
None
}
/// Get processing order for tables based on dependencies
/// Tables with no dependencies come first, then tables that depend only on already-processed tables
pub async fn get_table_processing_order(
db_pool: &PgPool,
schema_id: i64,
) -> Result<Vec<String>, sqlx::Error> {
let all_tables = sqlx::query!(
"SELECT id, table_name FROM table_definitions WHERE schema_id = $1 ORDER BY table_name",
schema_id
)
.fetch_all(db_pool)
.await?;
let dependencies = sqlx::query!(
r#"SELECT source_table_id, target_table_id
FROM script_dependencies sd
JOIN table_definitions td ON sd.source_table_id = td.id
WHERE td.schema_id = $1"#,
schema_id
)
.fetch_all(db_pool)
.await?;
// Build dependency graph
let mut graph: HashMap<i64, Vec<i64>> = HashMap::new();
let mut in_degree: HashMap<i64, usize> = HashMap::new();
let mut table_names: HashMap<i64, String> = HashMap::new();
// Initialize all tables
for table in &all_tables {
in_degree.insert(table.id, 0);
table_names.insert(table.id, table.table_name.clone());
graph.insert(table.id, Vec::new());
}
// Build graph and calculate in-degrees
for dep in dependencies {
graph.entry(dep.target_table_id).or_default().push(dep.source_table_id);
*in_degree.entry(dep.source_table_id).or_insert(0) += 1;
}
// Topological sort using Kahn's algorithm
let mut queue: Vec<i64> = in_degree
.iter()
.filter(|(_, &degree)| degree == 0)
.map(|(&id, _)| id)
.collect();
let mut result = Vec::new();
while let Some(node) = queue.pop() {
result.push(table_names[&node].clone());
if let Some(neighbors) = graph.get(&node) {
for &neighbor in neighbors {
if let Some(degree) = in_degree.get_mut(&neighbor) {
*degree -= 1;
if *degree == 0 {
queue.push(neighbor);
}
}
}
}
}
// If result doesn't contain all tables, there are cycles
if result.len() != all_tables.len() {
return Err(sqlx::Error::Protocol("Circular dependencies detected - cannot determine processing order".into()));
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cycle_detection_simple() {
// Test the cycle detection logic with a simple case
let mut graph = HashMap::new();
graph.insert(1, vec![2]);
graph.insert(2, vec![1]); // 1 -> 2 -> 1 (cycle)
let mut table_names = HashMap::new();
table_names.insert(1, "table_a".to_string());
table_names.insert(2, "table_b".to_string());
let mut visited = std::collections::HashSet::new();
let mut rec_stack = std::collections::HashSet::new();
let cycle = detect_cycle_dfs(1, &graph, &mut visited, &mut rec_stack, &table_names);
assert!(cycle.is_some());
let cycle_str = cycle.unwrap();
assert!(cycle_str.contains("table_a") && cycle_str.contains("table_b"));
}
}

View File

@@ -1,14 +1,357 @@
// src/table_script/handlers/post_table_script.rs
// TODO MAKE THE SCRIPTS PUSH ONLY TO THE EMPTY FILES
use tonic::Status;
use sqlx::{PgPool, Error as SqlxError};
use common::proto::multieko2::table_script::{PostTableScriptRequest, TableScriptResponse};
use common::proto::komp_ac::table_script::{PostTableScriptRequest, TableScriptResponse};
use serde_json::Value;
use crate::steel::server::syntax_parser::SyntaxParser;
use steel_decimal::SteelDecimal;
use regex::Regex;
use std::collections::HashSet;
use std::collections::HashMap;
use crate::table_script::handlers::dependency_analyzer::DependencyAnalyzer;
const SYSTEM_COLUMNS: &[&str] = &["id", "deleted", "created_at"];
// TODO MAKE SCRIPT PUSH ONLY TO THE EMPTY TABLES
/// Validates the target column and ensures it is not a system column.
// Define prohibited data types for Steel scripts (boolean is explicitly allowed)
const PROHIBITED_TYPES: &[&str] = &["BIGINT", "DATE", "TIMESTAMPTZ"];
const MATH_PROHIBITED_TYPES: &[&str] = &["BIGINT", "TEXT", "BOOLEAN", "DATE", "TIMESTAMPTZ"];
// Math operations that Steel Decimal will transform
const MATH_OPERATIONS: &[&str] = &[
"+", "-", "*", "/", "^", "**", "pow", "sqrt",
">", "<", "=", ">=", "<=", "min", "max", "abs",
"round", "ln", "log", "log10", "exp", "sin", "cos", "tan"
];
#[derive(Debug, Clone)]
enum SExpr {
Atom(String),
List(Vec<SExpr>),
}
#[derive(Debug)]
struct Parser {
tokens: Vec<String>,
position: usize,
}
impl Parser {
fn new(script: &str) -> Self {
let tokens = Self::tokenize(script);
Self { tokens, position: 0 }
}
fn tokenize(script: &str) -> Vec<String> {
let mut tokens = Vec::new();
let mut current_token = String::new();
let mut in_string = false;
let mut escape_next = false;
for ch in script.chars() {
if escape_next {
current_token.push(ch);
escape_next = false;
continue;
}
match ch {
'\\' if in_string => {
escape_next = true;
current_token.push(ch);
}
'"' => {
current_token.push(ch);
if in_string {
// End of string - push the complete string token
tokens.push(current_token.clone());
current_token.clear();
}
in_string = !in_string;
}
'(' | ')' if !in_string => {
if !current_token.is_empty() {
tokens.push(current_token.clone());
current_token.clear();
}
tokens.push(ch.to_string());
}
ch if ch.is_whitespace() && !in_string => {
if !current_token.is_empty() {
tokens.push(current_token.clone());
current_token.clear();
}
}
_ => {
current_token.push(ch);
}
}
}
if !current_token.is_empty() {
tokens.push(current_token);
}
tokens
}
fn parse(&mut self) -> Result<Vec<SExpr>, String> {
let mut expressions = Vec::new();
while self.position < self.tokens.len() {
expressions.push(self.parse_expr()?);
}
Ok(expressions)
}
fn parse_expr(&mut self) -> Result<SExpr, String> {
if self.position >= self.tokens.len() {
return Err("Unexpected end of input".to_string());
}
let token = &self.tokens[self.position];
if token == "(" {
self.position += 1; // consume '('
let mut elements = Vec::new();
while self.position < self.tokens.len() && self.tokens[self.position] != ")" {
elements.push(self.parse_expr()?);
}
if self.position >= self.tokens.len() {
return Err("Missing closing parenthesis".to_string());
}
self.position += 1; // consume ')'
Ok(SExpr::List(elements))
} else {
self.position += 1;
Ok(SExpr::Atom(token.clone()))
}
}
}
#[derive(Debug)]
struct MathValidator {
column_references: Vec<(String, String)>, // (table, column) pairs found in math contexts
}
impl MathValidator {
fn new() -> Self {
Self {
column_references: Vec::new(),
}
}
fn validate_expressions(&mut self, expressions: &[SExpr]) -> Result<(), String> {
for expr in expressions {
self.check_expression(expr, false)?;
}
Ok(())
}
fn check_expression(&mut self, expr: &SExpr, in_math_context: bool) -> Result<(), String> {
match expr {
SExpr::Atom(_) => Ok(()),
SExpr::List(elements) => {
if elements.is_empty() {
return Ok(());
}
// Check if this is a math operation
let is_math = if let SExpr::Atom(op) = &elements[0] {
MATH_OPERATIONS.contains(&op.as_str())
} else {
false
};
// Check if this is a column access function
if let SExpr::Atom(func) = &elements[0] {
if func == "steel_get_column" && in_math_context {
self.extract_column_reference_from_steel_get_column(elements)?;
} else if func == "steel_get_column_with_index" && in_math_context {
self.extract_column_reference_from_steel_get_column_with_index(elements)?;
}
}
// Recursively check all elements, marking math context appropriately
for element in &elements[1..] { // Skip the operator/function name
self.check_expression(element, in_math_context || is_math)?;
}
Ok(())
}
}
}
fn extract_column_reference_from_steel_get_column(&mut self, elements: &[SExpr]) -> Result<(), String> {
// (steel_get_column "table" "column")
if elements.len() >= 3 {
if let (SExpr::Atom(table), SExpr::Atom(column)) = (&elements[1], &elements[2]) {
let table_name = self.unquote_string(table)?;
let column_name = self.unquote_string(column)?;
self.column_references.push((table_name, column_name));
}
}
Ok(())
}
fn extract_column_reference_from_steel_get_column_with_index(&mut self, elements: &[SExpr]) -> Result<(), String> {
// (steel_get_column_with_index "table" index "column")
if elements.len() >= 4 {
if let (SExpr::Atom(table), SExpr::Atom(column)) = (&elements[1], &elements[3]) {
let table_name = self.unquote_string(table)?;
let column_name = self.unquote_string(column)?;
self.column_references.push((table_name, column_name));
}
}
Ok(())
}
fn unquote_string(&self, s: &str) -> Result<String, String> {
if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
Ok(s[1..s.len()-1].to_string())
} else {
Err(format!("Expected quoted string, got: {}", s))
}
}
}
/// Valide script is not empty
fn validate_script_basic_syntax(script: &str) -> Result<(), Status> {
let trimmed = script.trim();
// Check for empty script
if trimmed.is_empty() {
return Err(Status::invalid_argument("Script cannot be empty"));
}
// Basic parentheses balance check
let mut paren_count = 0;
for ch in trimmed.chars() {
match ch {
'(' => paren_count += 1,
')' => {
paren_count -= 1;
if paren_count < 0 {
return Err(Status::invalid_argument("Unbalanced parentheses: closing ')' without matching opening '('"));
}
},
_ => {}
}
}
if paren_count != 0 {
return Err(Status::invalid_argument("Unbalanced parentheses: missing closing parentheses"));
}
// Check for basic S-expression structure
if !trimmed.starts_with('(') {
return Err(Status::invalid_argument("Script must start with an opening parenthesis '('"));
}
Ok(())
}
/// Parse Steel script and extract column references used in mathematical contexts
fn extract_math_column_references(script: &str) -> Result<Vec<(String, String)>, String> {
let mut parser = Parser::new(script);
let expressions = parser.parse()
.map_err(|e| format!("Parse error: {}", e))?;
let mut validator = MathValidator::new();
validator.validate_expressions(&expressions)
.map_err(|e| format!("Validation error: {}", e))?;
Ok(validator.column_references)
}
/// Validate that mathematical operations don't use TEXT or BOOLEAN columns
async fn validate_math_operations_column_types(
db_pool: &PgPool,
schema_id: i64,
script: &str,
) -> Result<(), Status> {
// Extract column references from mathematical contexts using proper S-expression parsing
let column_refs = extract_math_column_references(script)
.map_err(|e| Status::invalid_argument(format!("Script parsing failed: {}", e)))?;
if column_refs.is_empty() {
return Ok(()); // No column references in math operations
}
// Get all unique table names referenced in math operations
let table_names: HashSet<String> = column_refs.iter()
.map(|(table, _)| table.clone())
.collect();
// Fetch table definitions for all referenced tables
let table_definitions = sqlx::query!(
r#"SELECT table_name, columns FROM table_definitions
WHERE schema_id = $1 AND table_name = ANY($2)"#,
schema_id,
&table_names.into_iter().collect::<Vec<_>>()
)
.fetch_all(db_pool)
.await
.map_err(|e| Status::internal(format!("Failed to fetch table definitions: {}", e)))?;
// Build a map of table_name -> column_name -> column_type
let mut table_column_types: HashMap<String, HashMap<String, String>> = HashMap::new();
for table_def in table_definitions {
let columns: Vec<String> = serde_json::from_value(table_def.columns)
.map_err(|e| Status::internal(format!("Invalid column data for table '{}': {}", table_def.table_name, e)))?;
let mut column_types = HashMap::new();
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('"');
column_types.insert(column_name.to_string(), data_type.to_string());
}
}
table_column_types.insert(table_def.table_name, column_types);
}
// Check each column reference in mathematical operations
for (table_name, column_name) in column_refs {
if let Some(table_columns) = table_column_types.get(&table_name) {
if let Some(column_type) = table_columns.get(&column_name) {
let normalized_type = normalize_data_type(column_type);
// Check if this type is prohibited in math operations
if MATH_PROHIBITED_TYPES.iter().any(|&prohibited| normalized_type.starts_with(prohibited)) {
return Err(Status::invalid_argument(format!(
"Cannot use column '{}' of type '{}' from table '{}' in mathematical operations. Mathematical operations cannot use columns of type: {}",
column_name,
column_type,
table_name,
MATH_PROHIBITED_TYPES.join(", ")
)));
}
} else {
return Err(Status::invalid_argument(format!(
"Script references column '{}' in table '{}' but this column does not exist",
column_name,
table_name
)));
}
} else {
return Err(Status::invalid_argument(format!(
"Script references table '{}' in mathematical operations but this table does not exist in this schema",
table_name
)));
}
}
Ok(())
}
/// Validates the target column and ensures it is not a system column or prohibited type.
/// Returns the column type if valid.
fn validate_target_column(
table_name: &str,
@@ -35,32 +378,237 @@ fn validate_target_column(
.collect();
// Find the target column and return its type
column_info
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
if is_prohibited_type(&column_type) {
return Err(format!(
"Cannot create script for column '{}' with type '{}'. Steel scripts cannot target columns of type: {}",
target,
column_type,
PROHIBITED_TYPES.join(", ")
));
}
// Add helpful info for boolean columns
let normalized_type = normalize_data_type(&column_type);
if normalized_type == "BOOLEAN" || normalized_type == "BOOL" {
println!("Info: Target column '{}' is boolean type. Values will be converted to Steel format (#true/#false)", target);
}
Ok(column_type)
}
/// Handles the creation of a new table script.
/// Check if a data type is prohibited for Steel scripts
/// Note: BOOLEAN/BOOL is explicitly allowed and handled with special conversion
fn is_prohibited_type(data_type: &str) -> bool {
let normalized_type = normalize_data_type(data_type);
PROHIBITED_TYPES.iter().any(|&prohibited| normalized_type.starts_with(prohibited))
}
/// Normalize data type for comparison (handle NUMERIC variations, etc.)
fn normalize_data_type(data_type: &str) -> String {
data_type.to_uppercase()
.split('(') // Remove precision/scale from NUMERIC(x,y)
.next()
.unwrap_or(data_type)
.trim()
.to_string()
}
/// Parse Steel script to extract all table/column references
fn extract_column_references_from_script(script: &str) -> Vec<(String, String)> {
let mut references = Vec::new();
// Regex patterns to match Steel function calls
let patterns = [
// (steel_get_column "table_name" "column_name")
r#"\(steel_get_column\s+"([^"]+)"\s+"([^"]+)"\)"#,
// (steel_get_column_with_index "table_name" index "column_name")
r#"\(steel_get_column_with_index\s+"([^"]+)"\s+\d+\s+"([^"]+)"\)"#,
];
for pattern in &patterns {
if let Ok(re) = Regex::new(pattern) {
for cap in re.captures_iter(script) {
if let (Some(table), Some(column)) = (cap.get(1), cap.get(2)) {
references.push((table.as_str().to_string(), column.as_str().to_string()));
}
}
}
}
// Also check for steel_get_column_with_index pattern (table, column are in different positions)
if let Ok(re) = Regex::new(r#"\(steel_get_column_with_index\s+"([^"]+)"\s+\d+\s+"([^"]+)"\)"#) {
for cap in re.captures_iter(script) {
if let (Some(table), Some(column)) = (cap.get(1), cap.get(2)) {
references.push((table.as_str().to_string(), column.as_str().to_string()));
}
}
}
references
}
/// Validate that script doesn't reference prohibited column types by checking actual DB schema
async fn validate_script_column_references(
db_pool: &PgPool,
schema_id: i64,
script: &str,
) -> Result<(), Status> {
// Extract all table/column references from the script
let references = extract_column_references_from_script(script);
if references.is_empty() {
return Ok(()); // No column references to validate
}
// Get all unique table names referenced in the script
let table_names: HashSet<String> = references.iter()
.map(|(table, _)| table.clone())
.collect();
// Fetch table definitions for all referenced tables
for table_name in table_names {
// Query the actual table definition from the database
let table_def = sqlx::query!(
r#"SELECT table_name, columns FROM table_definitions
WHERE schema_id = $1 AND table_name = $2"#,
schema_id,
table_name
)
.fetch_optional(db_pool)
.await
.map_err(|e| Status::internal(format!("Failed to fetch table definition for '{}': {}", table_name, e)))?;
if let Some(table_def) = table_def {
// Check each column reference for this table
for (ref_table, ref_column) in &references {
if ref_table == &table_name {
// Validate this specific column reference
if let Err(error_msg) = validate_referenced_column_type(&table_name, ref_column, &table_def.columns) {
return Err(Status::invalid_argument(error_msg));
}
}
}
} else {
return Err(Status::invalid_argument(format!(
"Script references table '{}' which does not exist in this schema",
table_name
)));
}
}
Ok(())
}
/// 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> {
// Parse the columns JSON into a vector of strings
let columns: Vec<String> = serde_json::from_value(table_columns.clone())
.map_err(|e| format!("Invalid column data for table '{}': {}", table_name, e))?;
// Extract column names and types
let column_info: Vec<(&str, &str)> = columns
.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!(
"Script references column '{}' in table '{}' which has prohibited type '{}'. Steel scripts cannot access columns of type: {}",
column_name,
table_name,
column_type,
PROHIBITED_TYPES.join(", ")
));
}
// Log info for boolean columns
let normalized_type = normalize_data_type(column_type);
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);
}
} else {
return Err(format!(
"Script references column '{}' in table '{}' but this column does not exist",
column_name,
table_name
));
}
Ok(())
}
/// Parse Steel SQL queries to check for prohibited type usage (basic heuristic)
fn validate_sql_queries_in_script(script: &str) -> Result<(), String> {
// Look for steel_query_sql calls
if let Ok(re) = Regex::new(r#"\(steel_query_sql\s+"([^"]+)"\)"#) {
for cap in re.captures_iter(script) {
if let Some(query) = cap.get(1) {
let sql = query.as_str().to_uppercase();
// Basic heuristic checks for prohibited type operations
let prohibited_patterns = [
"EXTRACT(",
"DATE_PART(",
"::DATE",
"::TIMESTAMPTZ",
"::BIGINT",
"CAST(", // Could be casting to prohibited types
];
for pattern in &prohibited_patterns {
if sql.contains(pattern) {
return Err(format!(
"Script contains SQL query with potentially prohibited type operations: '{}'. Steel scripts cannot use operations on types: {}",
query.as_str(),
PROHIBITED_TYPES.join(", ")
));
}
}
}
}
}
Ok(())
}
/// Handles the creation of a new table script with dependency validation.
pub async fn post_table_script(
db_pool: &PgPool,
request: PostTableScriptRequest,
) -> Result<TableScriptResponse, Status> {
// Basic script validation first
validate_script_basic_syntax(&request.script)?;
// Start a transaction for ALL operations - critical for atomicity
let mut tx = db_pool.begin().await
.map_err(|e| Status::internal(format!("Failed to start transaction: {}", e)))?;
// Fetch the table definition
let table_def = sqlx::query!(
r#"SELECT id, table_name, columns, schema_id
FROM table_definitions WHERE id = $1"#,
request.table_definition_id
)
.fetch_optional(db_pool)
.fetch_optional(&mut *tx)
.await
.map_err(|e| {
Status::internal(format!("Failed to fetch table definition: {}", e))
})?
.map_err(|e| Status::internal(format!("Failed to fetch table definition: {}", e)))?
.ok_or_else(|| Status::not_found("Table definition not found"))?;
// Validate the target column and get its type
// Validate the target column and get its type (includes prohibited type check)
let column_type = validate_target_column(
&table_def.table_name,
&request.target_column,
@@ -68,16 +616,46 @@ pub async fn post_table_script(
)
.map_err(|e| Status::invalid_argument(e))?;
// Parse and transform the script using the syntax parser
let parser = SyntaxParser::new();
let parsed_script = parser.parse(&request.script, &table_def.table_name);
// REORDER: Math validation FIRST so we get specific error messages for math operations
validate_math_operations_column_types(db_pool, table_def.schema_id, &request.script).await?;
// Insert the script into the database
// THEN general column validation (catches non-math prohibited access)
validate_script_column_references(db_pool, table_def.schema_id, &request.script).await?;
// Validate SQL queries in script for prohibited type operations
validate_sql_queries_in_script(&request.script)
.map_err(|e| Status::invalid_argument(e))?;
// Create dependency analyzer for this schema
let analyzer = DependencyAnalyzer::new(table_def.schema_id);
// Analyze script dependencies
let dependencies = analyzer
.analyze_script_dependencies(&request.script, &table_def.table_name)
.map_err(|e| Status::from(e))?;
// Check for circular dependencies BEFORE making any changes
// Pass the transaction to ensure we see any existing dependencies
analyzer
.check_for_cycles(&mut tx, table_def.id, &dependencies)
.await
.map_err(|e| Status::from(e))?;
// Transform the script using steel_decimal (this happens AFTER validation)
let steel_decimal = SteelDecimal::new();
let parsed_script = steel_decimal.transform(&request.script);
// Insert or update the script
let script_record = sqlx::query!(
r#"INSERT INTO table_scripts
(table_definitions_id, target_table, target_column,
target_column_type, script, description, schema_id)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (table_definitions_id, target_column)
DO UPDATE SET
script = EXCLUDED.script,
description = EXCLUDED.description,
target_column_type = EXCLUDED.target_column_type
RETURNING id"#,
request.table_definition_id,
table_def.table_name,
@@ -87,18 +665,83 @@ pub async fn post_table_script(
request.description,
table_def.schema_id
)
.fetch_one(db_pool)
.fetch_one(&mut *tx)
.await
.map_err(|e| match e {
SqlxError::Database(db_err) if db_err.constraint() == Some("table_scripts_table_definitions_id_target_column_key") => {
Status::already_exists("Script already exists for this column")
.map_err(|e| {
match e {
SqlxError::Database(db_err)
if db_err.constraint() == Some("table_scripts_table_definitions_id_target_column_key") => {
Status::already_exists("Script already exists for this column")
}
_ => Status::internal(format!("Failed to insert script: {}", e)),
}
_ => Status::internal(format!("Failed to insert script: {}", e)),
})?;
// Return the response with the new script ID
// Save the dependencies within the same transaction
analyzer
.save_dependencies(&mut tx, script_record.id, table_def.id, &dependencies)
.await
.map_err(|e| Status::from(e))?;
// Only now commit the entire transaction - script + dependencies together
tx.commit().await
.map_err(|e| Status::internal(format!("Failed to commit transaction: {}", e)))?;
// Generate warnings for potential issues
let warnings = generate_warnings(&dependencies, &table_def.table_name);
Ok(TableScriptResponse {
id: script_record.id,
warnings: String::new(), // No warnings for now
warnings,
})
}
/// Generate helpful warnings for script dependencies
fn generate_warnings(dependencies: &[crate::table_script::handlers::dependency_analyzer::Dependency], table_name: &str) -> String {
let mut warnings = Vec::new();
// Check for self-references
if dependencies.iter().any(|d| d.target_table == table_name) {
warnings.push("Warning: Script references its own table, which may cause issues during initial population.".to_string());
}
// Check for complex SQL queries
let sql_deps_count = dependencies.iter()
.filter(|d| matches!(d.dependency_type, crate::table_script::handlers::dependency_analyzer::DependencyType::SqlQuery { .. }))
.count();
if sql_deps_count > 0 {
warnings.push(format!(
"Warning: Script contains {} raw SQL quer{}, ensure they are read-only and reference valid tables.",
sql_deps_count,
if sql_deps_count == 1 { "y" } else { "ies" }
));
}
// Check for many dependencies
if dependencies.len() > 5 {
warnings.push(format!(
"Warning: Script depends on {} tables, which may affect processing performance.",
dependencies.len()
));
}
// Count structured access dependencies
let structured_deps_count = dependencies.iter()
.filter(|d| matches!(
d.dependency_type,
crate::table_script::handlers::dependency_analyzer::DependencyType::ColumnAccess { .. } |
crate::table_script::handlers::dependency_analyzer::DependencyType::IndexedAccess { .. }
))
.count();
if structured_deps_count > 0 {
warnings.push(format!(
"Info: Script uses {} linked table{} via steel_get_column functions.",
structured_deps_count,
if structured_deps_count == 1 { "" } else { "s" }
));
}
warnings.join(" ")
}

View File

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

View File

@@ -4,7 +4,7 @@ grpcurl -plaintext \
"table_name": "2025_customer"
}' \
localhost:50051 \
multieko2.table_structure.TableStructureService/GetTableStructure
komp_ac.table_structure.TableStructureService/GetTableStructure
{
"columns": [
{

View File

@@ -1,5 +1,5 @@
// src/table_structure/handlers/table_structure.rs
use common::proto::multieko2::table_structure::{
use common::proto::komp_ac::table_structure::{
GetTableStructureRequest, TableColumn, TableStructureResponse,
};
use sqlx::PgPool;

View File

@@ -2,7 +2,7 @@ grpcurl -plaintext -d '{
"profile_name": "default",
"table_name": "2025_adresar",
"position": 1
}' localhost:50051 multieko2.tables_data.TablesData/GetTableDataByPosition
}' localhost:50051 komp_ac.tables_data.TablesData/GetTableDataByPosition
{
"data": {
"banka": "New Banka",

View File

@@ -1,6 +1,6 @@
Valid get request:
grpcurl -plaintext -d '{"profile_name": "default", "table_name": "2025_adresar", "id": 2}' localhost:50051 multieko2.tables_data.TablesData/GetTableData
grpcurl -plaintext -d '{"profile_name": "default", "table_name": "2025_adresar", "id": 2}' localhost:50051 komp_ac.tables_data.TablesData/GetTableData
{
"data": {
@@ -26,7 +26,7 @@ grpcurl -plaintext -d '{"profile_name": "default", "table_name": "2025_adresar",
Request of a deleted data:
grpcurl -plaintext -d '{"profile_name": "default", "table_name": "2025_adresar", "id": 1}' localhost:50051 multieko2.tables_data.TablesData/GetTableData
grpcurl -plaintext -d '{"profile_name": "default", "table_name": "2025_adresar", "id": 1}' localhost:50051 komp_ac.tables_data.TablesData/GetTableData
ERROR:
Code: NotFound

View File

@@ -1,4 +1,4 @@
grpcurl -plaintext -d '{"profile_name": "default", "table_name": "2025_adresar"}' localhost:50051 multieko2.tables_data.TablesData/GetTableDataCount
grpcurl -plaintext -d '{"profile_name": "default", "table_name": "2025_adresar"}' localhost:50051 komp_ac.tables_data.TablesData/GetTableDataCount
{
"count": "1"

Some files were not shown because too many files have changed in this diff Show More