diff --git a/README.md b/README.md index 074eee65..ab900d28 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ which emphasizes **simplicity**, **extensibility** and **productivity**. [![Crates.io](https://img.shields.io/crates/v/zino)][zino] [![Documentation](https://shields.io/docsrs/zino)][zino-docs] -[![Downloads](https://img.shields.io/crates/d/zino)][zino] [![License](https://img.shields.io/crates/l/zino)][license] ## Highlights diff --git a/examples/actix-app/Cargo.toml b/examples/actix-app/Cargo.toml index 2ef8ca3b..d09b8855 100644 --- a/examples/actix-app/Cargo.toml +++ b/examples/actix-app/Cargo.toml @@ -13,7 +13,7 @@ serde_json = "1.0.104" tracing = "0.1.37" [dependencies.serde] -version = "1.0.178" +version = "1.0.180" features = ["derive"] [dependencies.zino] diff --git a/examples/actix-app/src/controller/file.rs b/examples/actix-app/src/controller/file.rs index 3858f607..a3adab5d 100644 --- a/examples/actix-app/src/controller/file.rs +++ b/examples/actix-app/src/controller/file.rs @@ -4,7 +4,7 @@ use zino::{prelude::*, Cluster, Request, Response, Result}; pub async fn upload(mut req: Request) -> Result { let (mut body, files) = req.parse_form_data::().await?; - let dir = Cluster::project_dir().join("assets/uploads"); + let dir = Cluster::relative_path("assets/uploads"); let expires = DateTime::now() + Duration::from_secs(600); let mut encryption_duration = Duration::ZERO; let mut uploads = Vec::new(); @@ -48,13 +48,13 @@ pub async fn decrypt(req: Request) -> Result { let secret_key = SecretAccessKey::new(&access_key_id); let security_token = req.parse_security_token(secret_key.as_ref())?; if security_token.is_expired() { - reject!(req, forbidden, "the seurity token has expired"); + reject!(req, forbidden, "the security token has expired"); } let Some(file_name) = query.get_str("file_name") else { reject!(req, "file_name", "it should be specified"); }; - let file_path = Cluster::project_dir().join(format!("assets/uploads/{file_name}")); + let file_path = Cluster::relative_path(format!("assets/uploads/{file_name}")); let mut file = NamedFile::try_from_local(file_path).extract(&req)?; let decryption_start_time = Instant::now(); diff --git a/examples/axum-app/Cargo.toml b/examples/axum-app/Cargo.toml index 598daab6..f1025b39 100644 --- a/examples/axum-app/Cargo.toml +++ b/examples/axum-app/Cargo.toml @@ -13,7 +13,7 @@ serde_json = "1.0.104" tracing = "0.1.37" [dependencies.serde] -version = "1.0.178" +version = "1.0.180" features = ["derive"] [dependencies.zino] diff --git a/examples/axum-app/src/controller/file.rs b/examples/axum-app/src/controller/file.rs index 3858f607..a3adab5d 100644 --- a/examples/axum-app/src/controller/file.rs +++ b/examples/axum-app/src/controller/file.rs @@ -4,7 +4,7 @@ use zino::{prelude::*, Cluster, Request, Response, Result}; pub async fn upload(mut req: Request) -> Result { let (mut body, files) = req.parse_form_data::().await?; - let dir = Cluster::project_dir().join("assets/uploads"); + let dir = Cluster::relative_path("assets/uploads"); let expires = DateTime::now() + Duration::from_secs(600); let mut encryption_duration = Duration::ZERO; let mut uploads = Vec::new(); @@ -48,13 +48,13 @@ pub async fn decrypt(req: Request) -> Result { let secret_key = SecretAccessKey::new(&access_key_id); let security_token = req.parse_security_token(secret_key.as_ref())?; if security_token.is_expired() { - reject!(req, forbidden, "the seurity token has expired"); + reject!(req, forbidden, "the security token has expired"); } let Some(file_name) = query.get_str("file_name") else { reject!(req, "file_name", "it should be specified"); }; - let file_path = Cluster::project_dir().join(format!("assets/uploads/{file_name}")); + let file_path = Cluster::relative_path(format!("assets/uploads/{file_name}")); let mut file = NamedFile::try_from_local(file_path).extract(&req)?; let decryption_start_time = Instant::now(); diff --git a/zino-core/Cargo.toml b/zino-core/Cargo.toml index 451f4313..43807fa9 100644 --- a/zino-core/Cargo.toml +++ b/zino-core/Cargo.toml @@ -110,7 +110,7 @@ reqwest-tracing = "0.4.5" rmp-serde = "1.1.2" serde_qs = "0.12.0" sha2 = "0.10.7" -sysinfo = "0.29.6" +sysinfo = "0.29.7" task-local-extensions = "0.1.4" toml = "0.7.6" tracing = "0.1.37" @@ -163,7 +163,7 @@ features = [ ] [dependencies.serde] -version = "1.0.178" +version = "1.0.180" features = ["derive"] [dependencies.serde_json] diff --git a/zino-core/src/application/mod.rs b/zino-core/src/application/mod.rs index 6fd2c9c5..463405ef 100644 --- a/zino-core/src/application/mod.rs +++ b/zino-core/src/application/mod.rs @@ -12,7 +12,12 @@ use crate::{ }; use reqwest::Response; use serde::de::DeserializeOwned; -use std::{env, fs, path::PathBuf, sync::LazyLock, thread}; +use std::{ + env, fs, + path::{Path, PathBuf}, + sync::LazyLock, + thread, +}; use toml::value::Table; use utoipa::openapi::{Info, OpenApi, OpenApiBuilder}; @@ -151,6 +156,12 @@ pub trait Application { LazyLock::force(&PROJECT_DIR) } + /// Returns the path relative to the project directory. + #[inline] + fn relative_path>(path: P) -> PathBuf { + PROJECT_DIR.join(path) + } + /// Returns the secret key for the application. /// It should have at least 64 bytes. /// diff --git a/zino-core/src/database/query.rs b/zino-core/src/database/query.rs index 377488ad..56d6f2ff 100644 --- a/zino-core/src/database/query.rs +++ b/zino-core/src/database/query.rs @@ -45,7 +45,7 @@ pub(super) trait QueryExt { } /// Formats projection fields. - fn format_fields(&self) -> Cow<'_, str> { + fn format_projection(&self) -> Cow<'_, str> { let fields = self.query_fields(); if fields.is_empty() { "*".into() @@ -133,9 +133,13 @@ pub(super) trait QueryExt { expression += &format!("WHERE {}", conditions.join(" AND ")); }; if let Some(groups) = filters.parse_str_array("$group") { - let groups = groups.join(", "); + let groups = groups + .into_iter() + .map(Self::format_field) + .collect::>() + .join(", "); expression += &format!(" GROUP BY {groups}"); - if let Some(JsonValue::Object(selection)) = filters.get("$match") { + if let Some(JsonValue::Object(selection)) = filters.get("$having") { let condition = Self::format_selection::(selection, " AND "); expression += &format!(" HAVING {condition}"); } diff --git a/zino-core/src/database/schema.rs b/zino-core/src/database/schema.rs index 49a4dc4f..84683b36 100644 --- a/zino-core/src/database/schema.rs +++ b/zino-core/src/database/schema.rs @@ -592,7 +592,7 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { Self::before_query(query).await?; let table_name = Self::table_name(); - let projection = query.format_fields(); + let projection = query.format_projection(); let filters = query.format_filters::(); let sort = query.format_sort(); let pagination = query.format_pagination(); @@ -634,7 +634,7 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { Self::before_query(query).await?; let table_name = Self::table_name(); - let projection = query.format_fields(); + let projection = query.format_projection(); let filters = query.format_filters::(); let sort = query.format_sort(); let sql = format!("SELECT {projection} FROM {table_name} {filters} {sort} LIMIT 1;"); @@ -677,7 +677,7 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { Self::before_query(query).await?; let table_name = Self::table_name(); - let projection = query.format_fields(); + let projection = query.format_projection(); let filters = query.format_filters::(); let sort = query.format_sort(); let sql = format!("SELECT {projection} FROM {table_name} {filters} {sort} LIMIT 1;"); @@ -701,7 +701,7 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { Self::before_query(query).await?; let table_name = Self::table_name(); - let projection = query.format_fields(); + let projection = query.format_projection(); let filters = query.format_filters::(); let sort = query.format_sort(); let pagination = query.format_pagination(); @@ -751,7 +751,7 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { } let table_name = Self::table_name(); - let projection = query.format_fields(); + let projection = query.format_projection(); let filters = query.format_filters::(); let sql = format!("SELECT {projection} FROM {table_name} {filters};"); @@ -829,7 +829,7 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { } let table_name = Self::table_name(); - let projection = query.format_fields(); + let projection = query.format_projection(); let filters = query.format_filters::(); let sql = format!("SELECT {projection} FROM {table_name} {filters};"); @@ -892,7 +892,7 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { let model_name = Query::format_field(Self::model_name()); let other_table_name = M::table_name(); let other_model_name = Query::format_field(M::model_name()); - let projection = query.format_fields(); + let projection = query.format_projection(); let filters = query.format_filters::(); let sort = query.format_sort(); let pagination = query.format_pagination(); @@ -1187,7 +1187,7 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { let table_name = Self::table_name(); let primary_key_name = Self::PRIMARY_KEY_NAME; let query = Self::default_query(); - let projection = query.format_fields(); + let projection = query.format_projection(); let sql = if cfg!(feature = "orm-mysql") { let placeholder = Query::placeholder(1); format!( @@ -1225,7 +1225,7 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { let table_name = Self::table_name(); let primary_key_name = Self::PRIMARY_KEY_NAME; let query = Self::default_query(); - let projection = query.format_fields(); + let projection = query.format_projection(); let sql = if cfg!(feature = "orm-mysql") { let placeholder = Query::placeholder(1); format!( diff --git a/zino-core/src/model/mutation.rs b/zino-core/src/model/mutation.rs index 6cbf462f..2d1f2c82 100644 --- a/zino-core/src/model/mutation.rs +++ b/zino-core/src/model/mutation.rs @@ -93,7 +93,7 @@ impl Mutation { } } -/// A builder for model mutations. +/// A builder type for model mutations. #[derive(Debug, Default)] pub struct MutationBuilder { // Editable fields. diff --git a/zino-core/src/model/query.rs b/zino-core/src/model/query.rs index 30c5fe91..07280f93 100644 --- a/zino-core/src/model/query.rs +++ b/zino-core/src/model/query.rs @@ -369,23 +369,47 @@ impl QueryBuilder { .upsert(field.into(), Map::from_entry("$nin", list)); } + /// Adds a filter with the condition for a field whose value is within a given range. + pub fn and_between(&mut self, field: S, min: T, max: T) + where + S: Into, + T: Into, + { + self.filters.upsert( + field.into(), + Map::from_entry("$range", vec![min.into(), max.into()]), + ); + } + + /// Adds a filter which groups rows that have the same values into summary rows. + #[inline] + pub fn group_by>(&mut self, fields: T) { + self.filters.upsert("$group", fields); + } + + /// Adds a filter which can be used with aggregate functions. + #[inline] + pub fn having>(&mut self, selection: T) { + self.filters.upsert("$having", selection); + } + /// Adds a sort with the specific order. #[inline] - pub fn order_by(&mut self, field: impl Into, descending: bool) -> &mut Self { + pub fn order_by>(&mut self, field: S, descending: bool) -> &mut Self { self.sort_order.push((field.into(), descending)); self } /// Adds a sort with the ascending order. #[inline] - pub fn order_asc(&mut self, field: impl Into) -> &mut Self { + pub fn order_asc>(&mut self, field: S) -> &mut Self { self.sort_order.push((field.into(), false)); self } /// Adds a sort with the descending order. #[inline] - pub fn order_desc(&mut self, field: impl Into) -> &mut Self { + pub fn order_desc>(&mut self, field: S) -> &mut Self { self.sort_order.push((field.into(), true)); self } diff --git a/zino-derive/Cargo.toml b/zino-derive/Cargo.toml index 7ed26118..948fd70e 100644 --- a/zino-derive/Cargo.toml +++ b/zino-derive/Cargo.toml @@ -17,7 +17,7 @@ proc-macro = true convert_case = "0.6.0" proc-macro2 = "1.0.66" quote = "1.0.32" -syn = "2.0.27" +syn = "2.0.28" [dependencies.zino-core] path = "../zino-core" diff --git a/zino-model/Cargo.toml b/zino-model/Cargo.toml index 6d965dee..a2ab17fb 100644 --- a/zino-model/Cargo.toml +++ b/zino-model/Cargo.toml @@ -33,7 +33,7 @@ regex = "1.9.1" strum_macros = "0.25.1" [dependencies.serde] -version = "1.0.178" +version = "1.0.180" features = ["derive"] [dependencies.zino-core] diff --git a/zino/Cargo.toml b/zino/Cargo.toml index 2557aefa..e8161f94 100644 --- a/zino/Cargo.toml +++ b/zino/Cargo.toml @@ -44,7 +44,7 @@ cfg-if = "1.0" futures = "0.3.28" hyper = "0.14.27" parking_lot = "0.12.1" -serde = "1.0.178" +serde = "1.0.180" serde_json = "1.0.104" toml = "0.7.6" tracing = "0.1.37" diff --git a/zino/src/application/actix_cluster.rs b/zino/src/application/actix_cluster.rs index a6f68e7f..53455248 100644 --- a/zino/src/application/actix_cluster.rs +++ b/zino/src/application/actix_cluster.rs @@ -50,7 +50,7 @@ impl Application for ActixCluster { // Server config let mut body_limit = 100 * 1024 * 1024; // 100MB let mut public_dir = PathBuf::new(); - let default_public_dir = Self::project_dir().join("public"); + let default_public_dir = Self::relative_path("public"); if let Some(server) = Self::config().get_table("server") { if let Some(limit) = server.get_usize("body-limit") { body_limit = limit; diff --git a/zino/src/application/axum_cluster.rs b/zino/src/application/axum_cluster.rs index eb8575d5..684932e2 100644 --- a/zino/src/application/axum_cluster.rs +++ b/zino/src/application/axum_cluster.rs @@ -69,7 +69,7 @@ impl Application for AxumCluster { let mut body_limit = 100 * 1024 * 1024; // 100MB let mut request_timeout = Duration::from_secs(10); // 10 seconds let mut public_dir = PathBuf::new(); - let default_public_dir = Self::project_dir().join("public"); + let default_public_dir = Self::relative_path("public"); if let Some(server) = Self::config().get_table("server") { if let Some(limit) = server.get_usize("body-limit") { body_limit = limit;