From 00c0a399cd6d3dbdf095c5aff5bc17b2716326bb Mon Sep 17 00:00:00 2001 From: filipriec Date: Fri, 25 Jul 2025 22:38:34 +0200 Subject: [PATCH 1/5] sql search2 added --- common/build.rs | 1 + common/proto/search2.proto | 46 ++ common/src/lib.rs | 3 + common/src/proto/descriptor.bin | Bin 26768 -> 29603 bytes common/src/proto/komp_ac.search2.rs | 394 ++++++++++++++++++ server/src/server/run.rs | 10 +- server/src/server/services/mod.rs | 2 + server/src/server/services/search2_service.rs | 202 +++++++++ 8 files changed, 654 insertions(+), 4 deletions(-) create mode 100644 common/proto/search2.proto create mode 100644 common/src/proto/komp_ac.search2.rs create mode 100644 server/src/server/services/search2_service.rs diff --git a/common/build.rs b/common/build.rs index d90d046..f65f5e1 100644 --- a/common/build.rs +++ b/common/build.rs @@ -15,6 +15,7 @@ fn main() -> Result<(), Box> { "proto/tables_data.proto", "proto/table_script.proto", "proto/search.proto", + "proto/search2.proto", ], &["proto"], )?; diff --git a/common/proto/search2.proto b/common/proto/search2.proto new file mode 100644 index 0000000..abdd19a --- /dev/null +++ b/common/proto/search2.proto @@ -0,0 +1,46 @@ +// In common/proto/search2.proto +syntax = "proto3"; +package komp_ac.search2; + +service Search2 { + rpc SearchTable(Search2Request) returns (Search2Response); +} + +message Search2Request { + string profile_name = 1; + string table_name = 2; + repeated ColumnFilter column_filters = 3; + optional string text_query = 4; // Optional fallback text search + optional int32 limit = 5; + optional string order_by = 6; + optional bool order_desc = 7; +} + +message ColumnFilter { + string column_name = 1; + FilterType filter_type = 2; + string value = 3; + optional string value2 = 4; // For range queries +} + +enum FilterType { + EQUALS = 0; + CONTAINS = 1; + STARTS_WITH = 2; + ENDS_WITH = 3; + RANGE = 4; + GREATER_THAN = 5; + LESS_THAN = 6; + IS_NULL = 7; + IS_NOT_NULL = 8; +} + +message Search2Response { + message Hit { + int64 id = 1; + string content_json = 2; // No score - this is SQL-based + optional string match_info = 3; // Info about which columns matched + } + repeated Hit hits = 1; + int32 total_count = 2; // Total matching records (for pagination) +} diff --git a/common/src/lib.rs b/common/src/lib.rs index 39f5759..8767dda 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -31,6 +31,9 @@ pub mod proto { pub mod search { include!("proto/komp_ac.search.rs"); } + pub mod search2 { + include!("proto/komp_ac.search2.rs"); + } pub const FILE_DESCRIPTOR_SET: &[u8] = include_bytes!("proto/descriptor.bin"); } diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 49e1c1eb79bb2e9d215194ea509f913a1058b03e..4e0c537ed9605dfcb120e9c3733760c5147e3b5f 100644 GIT binary patch delta 2738 zcmZ`*%Wm676cx!KMGf`xNS0;!AzE#0D}JDMiyO> zZj%+!T{i^`c+-8C-Sku1Wxt@m(Q{`=I!F+}3!nQq_ueyihPwVo`{O_Duj})V_;rmJ z0&h4Qo$u{l&iv39rI-H2}W_6v=4~LV%$iJF~au&18rRf1# z8ZU2t@oLc;&yW0R=uN}H4}m|Gak(yZlC~!>(FR-$!_oO*JU#Q(aI^u^b_7#$LbpM8 zuQkqZRT7+A2hX1GANR@`k>~bh4}zw`K@%tV>zA9oM-`v=`Y|8T!kHYxV_px0B-5)RKE^#+~i$H(QY05O55ePyus zzT??gF7Y*r%8Szfet0tRMDzW^V(o*l;l4jWoz#=xi`)`S%j{^p&9%HRw2WvpZ@xNP z_&mO_oZDQ}g`sEaoO9jE2*xrNgyM#k(S*Tlw9J{6(KEs@*_SuJoh?Nzp64c+4A^$= zQ6fX4ohi#iMv1mvqeLbXb9#eP2$;gi>2`t;n9I#42z0snVw?sS*XyxMLmE%gA{{r_ z;JHfTL>{j##CaIT(YIok&Is`YSwPN0fyjV^UF*n_~SELp6rP#$JN+=mf z3J$hQ%Emxan5!ui=?pA|#YW76$fDkiBTcXr(^gFKrLCA?DT?GMOpp{;R$~%KQ%XrJ zkd#sq3nV3{6c@0x2(X;6SYRoEg?2y_Ok@_+4$u(^yDHf=TAm71g93oB!qlKZ2&_12 zG$7Db;>k1}t}0WL0ieYyEAo5DYmJrq&7o<>u3J#5o`93Lnzu zD^>2|)HMP%)@)VWNB+#64W}!wBR`4SF5h_HD~_Q`&P2X-$%+U2olDlx*KwkLV1UPKs-Gu*|?;fwKfNH1|4H}PXS bADsl+R%=Lzcz9BRwArpNo>=ln>8Jk#!*r9* delta 9 QcmZ4doN>ZM#tjoP02mYmmH+?% diff --git a/common/src/proto/komp_ac.search2.rs b/common/src/proto/komp_ac.search2.rs new file mode 100644 index 0000000..496273e --- /dev/null +++ b/common/src/proto/komp_ac.search2.rs @@ -0,0 +1,394 @@ +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Search2Request { + #[prost(string, tag = "1")] + pub profile_name: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub table_name: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "3")] + pub column_filters: ::prost::alloc::vec::Vec, + /// Optional fallback text search + #[prost(string, optional, tag = "4")] + pub text_query: ::core::option::Option<::prost::alloc::string::String>, + #[prost(int32, optional, tag = "5")] + pub limit: ::core::option::Option, + #[prost(string, optional, tag = "6")] + pub order_by: ::core::option::Option<::prost::alloc::string::String>, + #[prost(bool, optional, tag = "7")] + pub order_desc: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ColumnFilter { + #[prost(string, tag = "1")] + pub column_name: ::prost::alloc::string::String, + #[prost(enumeration = "FilterType", tag = "2")] + pub filter_type: i32, + #[prost(string, tag = "3")] + pub value: ::prost::alloc::string::String, + /// For range queries + #[prost(string, optional, tag = "4")] + pub value2: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Search2Response { + #[prost(message, repeated, tag = "1")] + pub hits: ::prost::alloc::vec::Vec, + /// Total matching records (for pagination) + #[prost(int32, tag = "2")] + pub total_count: i32, +} +/// Nested message and enum types in `Search2Response`. +pub mod search2_response { + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct Hit { + #[prost(int64, tag = "1")] + pub id: i64, + /// No score - this is SQL-based + #[prost(string, tag = "2")] + pub content_json: ::prost::alloc::string::String, + /// Info about which columns matched + #[prost(string, optional, tag = "3")] + pub match_info: ::core::option::Option<::prost::alloc::string::String>, + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum FilterType { + Equals = 0, + Contains = 1, + StartsWith = 2, + EndsWith = 3, + Range = 4, + GreaterThan = 5, + LessThan = 6, + IsNull = 7, + IsNotNull = 8, +} +impl FilterType { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Equals => "EQUALS", + Self::Contains => "CONTAINS", + Self::StartsWith => "STARTS_WITH", + Self::EndsWith => "ENDS_WITH", + Self::Range => "RANGE", + Self::GreaterThan => "GREATER_THAN", + Self::LessThan => "LESS_THAN", + Self::IsNull => "IS_NULL", + Self::IsNotNull => "IS_NOT_NULL", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "EQUALS" => Some(Self::Equals), + "CONTAINS" => Some(Self::Contains), + "STARTS_WITH" => Some(Self::StartsWith), + "ENDS_WITH" => Some(Self::EndsWith), + "RANGE" => Some(Self::Range), + "GREATER_THAN" => Some(Self::GreaterThan), + "LESS_THAN" => Some(Self::LessThan), + "IS_NULL" => Some(Self::IsNull), + "IS_NOT_NULL" => Some(Self::IsNotNull), + _ => None, + } + } +} +/// Generated client implementations. +pub mod search2_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 Search2Client { + inner: tonic::client::Grpc, + } + impl Search2Client { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl Search2Client + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + 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( + inner: T, + interceptor: F, + ) -> Search2Client> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + Search2Client::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, + ) -> std::result::Result< + tonic::Response, + 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.search2.Search2/SearchTable", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("komp_ac.search2.Search2", "SearchTable")); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +pub mod search2_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 Search2Server. + #[async_trait] + pub trait Search2: std::marker::Send + std::marker::Sync + 'static { + async fn search_table( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + } + #[derive(Debug)] + pub struct Search2Server { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl Search2Server { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> 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( + inner: T, + interceptor: F, + ) -> InterceptedService + 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 tonic::codegen::Service> for Search2Server + where + T: Search2, + B: Body + std::marker::Send + 'static, + B::Error: Into + std::marker::Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/komp_ac.search2.Search2/SearchTable" => { + #[allow(non_camel_case_types)] + struct SearchTableSvc(pub Arc); + impl tonic::server::UnaryService + for SearchTableSvc { + type Response = super::Search2Response; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::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 Clone for Search2Server { + 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.search2.Search2"; + impl tonic::server::NamedService for Search2Server { + const NAME: &'static str = SERVICE_NAME; + } +} diff --git a/server/src/server/run.rs b/server/src/server/run.rs index dcd79cb..7f9be74 100644 --- a/server/src/server/run.rs +++ b/server/src/server/run.rs @@ -10,14 +10,16 @@ use crate::server::services::{ TableDefinitionService, TablesDataService, TableScriptService, - AuthServiceImpl + AuthServiceImpl, + Search2Service, }; use common::proto::komp_ac::{ table_structure::table_structure_service_server::TableStructureServiceServer, table_definition::table_definition_server::TableDefinitionServer, tables_data::tables_data_server::TablesDataServer, table_script::table_script_server::TableScriptServer, - auth::auth_service_server::AuthServiceServer + auth::auth_service_server::AuthServiceServer, + search2::search2_server::Search2Server, }; use search::{SearcherService, SearcherServer}; @@ -47,9 +49,8 @@ pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box Result<(), Box, + ) -> Result, Status> { + let req = request.into_inner(); + + // Build SQL query - NOW PASS DB_POOL AND AWAIT + let (sql, bind_values, count_sql) = build_search_query(&self.db_pool, &req).await + .map_err(|e| Status::invalid_argument(format!("Query build error: {}", e)))?; + + // Execute main query + let rows = execute_query(&self.db_pool, &sql, &bind_values).await + .map_err(|e| Status::internal(format!("Database error: {}", e)))?; + + // Execute count query for pagination + let total_count = execute_count_query(&self.db_pool, &count_sql, &bind_values).await + .map_err(|e| Status::internal(format!("Count query error: {}", e)))?; + + // Convert to response format + let hits = rows.into_iter().map(|row| { + Hit { + id: row.id, + content_json: row.content_json, + match_info: Some(format!("SQL search on table: {}", req.table_name)), + } + }).collect(); + + Ok(Response::new(Search2Response { + hits, + total_count, + })) + } +} + +#[derive(Debug)] +struct QueryRow { + id: i64, + content_json: String, +} + +// MAKE THIS FUNCTION ASYNC AND ADD DB_POOL PARAMETER +async fn build_search_query( + db_pool: &PgPool, + request: &Search2Request +) -> Result<(String, Vec, String), String> { + + // Since Search2Request doesn't have profile_name, use "default" + // You can add profile_name to the proto later if needed + let profile_name = "default"; + + // NOW AWAIT THE ASYNC FUNCTION CALL + let qualified_table = qualify_table_name_for_data(db_pool, profile_name, &request.table_name) + .await + .map_err(|e| format!("Invalid table name: {}", e))?; + + let mut conditions = vec!["deleted = FALSE".to_string()]; + let mut bind_values = Vec::new(); + + // Build WHERE conditions from column filters + for filter in &request.column_filters { + let condition = build_filter_condition(filter, &mut bind_values)?; + conditions.push(condition); + } + + // Add text search fallback if provided + if let Some(text_query) = &request.text_query { + if !text_query.trim().is_empty() { + bind_values.push(format!("%{}%", text_query.to_lowercase())); + conditions.push(format!( + "EXISTS (SELECT 1 FROM jsonb_each_text(to_jsonb(t)) as kv(key, value) WHERE LOWER(value) LIKE ${})", + bind_values.len() + )); + } + } + + let where_clause = conditions.join(" AND "); + + // Build ORDER BY clause + let order_clause = if let Some(order_by) = &request.order_by { + let direction = if request.order_desc.unwrap_or(false) { "DESC" } else { "ASC" }; + format!("ORDER BY \"{}\" {}", order_by, direction) + } else { + "ORDER BY id DESC".to_string() + }; + + let limit_clause = format!("LIMIT {}", request.limit.unwrap_or(100)); + + // Main query + let sql = format!( + "SELECT id, to_jsonb(t) AS data FROM {} t WHERE {} {} {}", + qualified_table, where_clause, order_clause, limit_clause + ); + + // Count query (for pagination) + let count_sql = format!( + "SELECT COUNT(*) FROM {} t WHERE {}", + qualified_table, where_clause + ); + + Ok((sql, bind_values, count_sql)) +} + +fn build_filter_condition(filter: &ColumnFilter, bind_values: &mut Vec) -> Result { + // FIX DEPRECATED WARNING - USE TryFrom INSTEAD + let filter_type = FilterType::try_from(filter.filter_type) + .map_err(|_| "Invalid filter type".to_string())?; + + let param_idx = bind_values.len() + 1; + + let condition = match filter_type { + FilterType::Equals => { + bind_values.push(filter.value.clone()); + format!("\"{}\" = ${}", filter.column_name, param_idx) + }, + FilterType::Contains => { + bind_values.push(format!("%{}%", filter.value)); + format!("\"{}\" ILIKE ${}", filter.column_name, param_idx) + }, + FilterType::StartsWith => { + bind_values.push(format!("{}%", filter.value)); + format!("\"{}\" ILIKE ${}", filter.column_name, param_idx) + }, + FilterType::EndsWith => { + bind_values.push(format!("%{}", filter.value)); + format!("\"{}\" ILIKE ${}", filter.column_name, param_idx) + }, + FilterType::Range => { + bind_values.push(filter.value.clone()); + bind_values.push(filter.value2.as_ref().unwrap_or(&"".to_string()).clone()); + format!("\"{}\" BETWEEN ${} AND ${}", filter.column_name, param_idx, param_idx + 1) + }, + FilterType::GreaterThan => { + bind_values.push(filter.value.clone()); + format!("\"{}\" > ${}", filter.column_name, param_idx) + }, + FilterType::LessThan => { + bind_values.push(filter.value.clone()); + format!("\"{}\" < ${}", filter.column_name, param_idx) + }, + FilterType::IsNull => { + format!("\"{}\" IS NULL", filter.column_name) + }, + FilterType::IsNotNull => { + format!("\"{}\" IS NOT NULL", filter.column_name) + }, + }; + + Ok(condition) +} + +async fn execute_query(pool: &PgPool, sql: &str, bind_values: &[String]) -> Result, sqlx::Error> { + let mut query = sqlx::query(sql); + + // Bind all parameters + for value in bind_values { + query = query.bind(value); + } + + let rows = query.fetch_all(pool).await?; + + let results = rows.into_iter().map(|row| { + QueryRow { + id: row.try_get("id").unwrap_or(0), + content_json: row.try_get::("data") + .unwrap_or(serde_json::Value::Null) + .to_string(), + } + }).collect(); + + Ok(results) +} + +async fn execute_count_query(pool: &PgPool, sql: &str, bind_values: &[String]) -> Result { + let mut query = sqlx::query(sql); + + // Bind all parameters + for value in bind_values { + query = query.bind(value); + } + + let row = query.fetch_one(pool).await?; + let count: i64 = row.try_get(0).unwrap_or(0); + + Ok(count as i32) +} From 263ccc32604dfec562f09b222acbfb4bc7eb694e Mon Sep 17 00:00:00 2001 From: filipriec Date: Sat, 26 Jul 2025 08:49:09 +0200 Subject: [PATCH 2/5] updated system --- Cargo.lock | 158 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 101 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a4fcd4..989a019 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -384,9 +384,9 @@ dependencies = [ [[package]] name = "bon" -version = "3.6.4" +version = "3.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61138465baf186c63e8d9b6b613b508cd832cba4ce93cf37ce5f096f91ac1a6" +checksum = "33d9ef19ae5263a138da9a86871eca537478ab0332a7770bac7e3f08b801f89f" dependencies = [ "bon-macros", "rustversion", @@ -394,11 +394,11 @@ dependencies = [ [[package]] name = "bon-macros" -version = "3.6.4" +version = "3.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40d1dad34aa19bf02295382f08d9bc40651585bd497266831d40ee6296fb49ca" +checksum = "577ae008f2ca11ca7641bd44601002ee5ab49ef0af64846ce1ab6057218a5cc1" dependencies = [ - "darling", + "darling 0.21.0", "ident_case", "prettyplease", "proc-macro2", @@ -478,18 +478,18 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "castaway" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" dependencies = [ "rustversion", ] [[package]] name = "cc" -version = "1.2.29" +version = "1.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ "jobserver", "libc", @@ -633,9 +633,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const_panic" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2459fc9262a1aa204eb4b5764ad4f189caec88aea9634389c0a25f8be7f6265e" +checksum = "b98d1483e98c9d67f341ab4b3915cfdc54740bd6f5cccc9226ee0535d86aa8fb" [[package]] name = "core-foundation" @@ -694,9 +694,9 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -791,8 +791,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a79c4acb1fd5fa3d9304be4c76e031c54d2e92d172a393e24b19a14fe8532fe9" +dependencies = [ + "darling_core 0.21.0", + "darling_macro 0.21.0", ] [[package]] @@ -809,13 +819,38 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "darling_core" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74875de90daf30eb59609910b84d4d368103aaec4c924824c6799b28f77d6a1d" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.104", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "darling_macro" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79f8e61677d5df9167cd85265f8e5f64b215cdea3fb55eebc3e622e44c7a146" +dependencies = [ + "darling_core 0.21.0", "quote", "syn 2.0.104", ] @@ -1410,9 +1445,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ "bytes", "futures-channel", @@ -1423,7 +1458,7 @@ dependencies = [ "hyper", "libc", "pin-project-lite", - "socket2", + "socket2 0.6.0", "tokio", "tower-service", "tracing", @@ -1592,9 +1627,9 @@ dependencies = [ [[package]] name = "im-lists" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88485149c4fcec01ebce4e4b8284a3c75b3d8a4749169f5481144e6433e9bcd2" +checksum = "8b971d2652e5700514cc92ca020dba64c790352af0ff2b9acb7514868a32d6aa" dependencies = [ "smallvec", ] @@ -1651,11 +1686,11 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" +checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" dependencies = [ - "darling", + "darling 0.20.11", "indoc", "proc-macro2", "quote", @@ -1664,9 +1699,9 @@ dependencies = [ [[package]] name = "io-uring" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" dependencies = [ "bitflags", "cfg-if", @@ -1783,9 +1818,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638" +checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" dependencies = [ "bitflags", "libc", @@ -1883,9 +1918,9 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memmap2" -version = "0.9.5" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28" dependencies = [ "libc", ] @@ -2273,17 +2308,16 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "polling" -version = "3.8.0" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50" +checksum = "8ee9b2fa7a4517d2c91ff5bc6c297a427a96749d15f98fcdbb22c05571a4d4b7" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.0.7", - "tracing", - "windows-sys 0.59.0", + "rustix 1.0.8", + "windows-sys 0.60.2", ] [[package]] @@ -2323,9 +2357,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.35" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" +checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" dependencies = [ "proc-macro2", "syn 2.0.104", @@ -2487,9 +2521,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", @@ -2595,9 +2629,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.13" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec" dependencies = [ "bitflags", ] @@ -2831,15 +2865,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2943,9 +2977,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" dependencies = [ "itoa", "memchr", @@ -2989,7 +3023,7 @@ dependencies = [ "lazy_static", "prost", "prost-types", - "rand 0.9.1", + "rand 0.9.2", "regex", "rstest", "rust-stemmers", @@ -3153,6 +3187,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "spin" version = "0.9.8" @@ -3427,7 +3471,7 @@ dependencies = [ "parking_lot", "polling", "quickscope", - "rand 0.9.1", + "rand 0.9.2", "serde", "serde_json", "smallvec", @@ -3579,9 +3623,9 @@ dependencies = [ [[package]] name = "tantivy" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2374a21157427c5faff2d90930f035b6c22a5d7b0e5b0b7f522e988ef33c06" +checksum = "64a966cb0e76e311f09cf18507c9af192f15d34886ee43d7ba7c7e3803660c43" dependencies = [ "aho-corasick", "arc-swap", @@ -3738,7 +3782,7 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix 1.0.7", + "rustix 1.0.8", "windows-sys 0.59.0", ] @@ -3853,7 +3897,7 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "slab", - "socket2", + "socket2 0.5.10", "tokio-macros", "windows-sys 0.52.0", ] @@ -3967,7 +4011,7 @@ dependencies = [ "percent-encoding", "pin-project", "prost", - "socket2", + "socket2 0.5.10", "tokio", "tokio-stream", "tower", @@ -4256,7 +4300,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" dependencies = [ - "darling", + "darling 0.20.11", "once_cell", "proc-macro-error2", "proc-macro2", @@ -4384,7 +4428,7 @@ checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" dependencies = [ "either", "env_home", - "rustix 1.0.7", + "rustix 1.0.8", "winsafe", ] @@ -4711,9 +4755,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] From 1fbc720620c506b0ac357c6ff7234740a63edb5c Mon Sep 17 00:00:00 2001 From: filipriec Date: Sat, 26 Jul 2025 19:05:08 +0200 Subject: [PATCH 3/5] updated --- Cargo.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 989a019..8a6af91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -974,7 +974,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -2860,7 +2860,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2873,7 +2873,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -3486,9 +3486,9 @@ dependencies = [ [[package]] name = "steel-decimal" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43950a3eed43f3e9765a51f5dc1b0de5e1687ba824b8589990747d9ba241187" +checksum = "4cd8a6d1a41d2146705b29292cac75c78a3e32d7b6cabb72d808209546615f37" dependencies = [ "regex", "rust_decimal", @@ -3783,7 +3783,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix 1.0.8", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4464,7 +4464,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] From c7353ac81ecc66604e48fede5bb181abb56fa932 Mon Sep 17 00:00:00 2001 From: filipriec Date: Sat, 26 Jul 2025 20:34:02 +0200 Subject: [PATCH 4/5] email is now required --- client/config.toml | 2 +- server/src/auth/handlers/register.rs | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/client/config.toml b/client/config.toml index cf791d2..a8462dd 100644 --- a/client/config.toml +++ b/client/config.toml @@ -74,7 +74,7 @@ move_left = [""] move_right = ["right"] suggestion_down = ["ctrl+n", "tab"] suggestion_up = ["ctrl+p", "shift+tab"] -trigger_autocomplete = ["left"] +trigger_autocomplete = ["tab"] [keybindings.command] exit_command_mode = ["ctrl+g", "esc"] diff --git a/server/src/auth/handlers/register.rs b/server/src/auth/handlers/register.rs index 63b6190..66a683f 100644 --- a/server/src/auth/handlers/register.rs +++ b/server/src/auth/handlers/register.rs @@ -11,6 +11,11 @@ pub async fn register( pool: &PgPool, payload: RegisterRequest, ) -> Result, Status> { + // Validate required fields + if payload.email.trim().is_empty() { + return Err(Status::invalid_argument("Email is required")); + } + // Validate passwords match if payload.password != payload.password_confirmation { return Err(Status::invalid_argument(AuthError::PasswordMismatch.to_string())); @@ -41,6 +46,15 @@ pub async fn register( if db_err.constraint() == Some("valid_roles") { return Status::invalid_argument(format!("Invalid role specified: '{}'", role_to_insert)); } + // Check for specific constraint violations + if let Some(constraint) = db_err.constraint() { + if constraint.contains("users_username_key") { + return Status::already_exists("Username already exists".to_string()); + } + if constraint.contains("users_email_key") { + return Status::already_exists("Email already exists".to_string()); + } + } } if e.to_string().contains("duplicate key") { Status::already_exists(AuthError::UserExists.to_string()) From a4e94878e7153419935b3213b12a2fc6c8ae4565 Mon Sep 17 00:00:00 2001 From: filipriec Date: Sat, 26 Jul 2025 22:30:45 +0200 Subject: [PATCH 5/5] enter decider should be taken care of next, suggestions works in register now also --- client/config.toml | 2 +- client/src/functions/modes/edit/auth_e.rs | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/client/config.toml b/client/config.toml index a8462dd..cf791d2 100644 --- a/client/config.toml +++ b/client/config.toml @@ -74,7 +74,7 @@ move_left = [""] move_right = ["right"] suggestion_down = ["ctrl+n", "tab"] suggestion_up = ["ctrl+p", "shift+tab"] -trigger_autocomplete = ["tab"] +trigger_autocomplete = ["left"] [keybindings.command] exit_command_mode = ["ctrl+g", "esc"] diff --git a/client/src/functions/modes/edit/auth_e.rs b/client/src/functions/modes/edit/auth_e.rs index 68b2301..9c717cf 100644 --- a/client/src/functions/modes/edit/auth_e.rs +++ b/client/src/functions/modes/edit/auth_e.rs @@ -291,6 +291,28 @@ pub async fn execute_edit_action( Ok("Moved to previous word end".to_string()) } + "trigger_autocomplete" => { + if let Some(register_state) = (state as &mut dyn Any).downcast_mut::() { + if register_state.current_field() == 4 { // Role field + register_state.update_role_suggestions(); + + if !register_state.role_suggestions.is_empty() { + register_state.in_suggestion_mode = true; + register_state.show_role_suggestions = true; + register_state.selected_suggestion_index = Some(0); + + return Ok("Autocomplete activated".to_string()); + } else { + return Ok("No suggestions available".to_string()); + } + } else { + return Ok("Autocomplete only available on role field".to_string()); + } + } else { + return Ok("Autocomplete not supported for this state".to_string()); + } + } + // --- Autocomplete Actions --- "suggestion_down" | "suggestion_up" | "select_suggestion" | "exit_suggestion_mode" => { // Attempt to downcast to RegisterState to handle suggestion logic here