From 33be95e5c4b01610081493d71ec1dc4e6121407d Mon Sep 17 00:00:00 2001 From: cosmod Date: Mon, 2 Dec 2024 23:10:33 +0800 Subject: [PATCH] Rename update_by_id to mutate_by_id --- examples/actix-app/src/controller/auth.rs | 4 +- examples/axum-app/src/controller/auth.rs | 50 ++++---- examples/ntex-app/src/controller/auth.rs | 4 +- zino-core/src/orm/accessor.rs | 4 +- zino-core/src/orm/mutation.rs | 19 +++ zino-core/src/orm/mysql.rs | 19 +-- zino-core/src/orm/postgres.rs | 17 +-- zino-core/src/orm/query.rs | 136 +++++++++++++++++++++- zino-core/src/orm/scalar.rs | 5 +- zino-core/src/orm/schema.rs | 87 ++++++++++++++ zino-core/src/orm/sqlite.rs | 27 +++-- zino/src/controller/mod.rs | 2 +- 12 files changed, 310 insertions(+), 64 deletions(-) diff --git a/examples/actix-app/src/controller/auth.rs b/examples/actix-app/src/controller/auth.rs index 24839b54..71bc40aa 100644 --- a/examples/actix-app/src/controller/auth.rs +++ b/examples/actix-app/src/controller/auth.rs @@ -16,7 +16,7 @@ pub async fn login(mut req: Request) -> Result { }); let mut user_mutations = user_updates.into_map_opt().unwrap_or_default(); - let (validation, user) = User::update_by_id(&user_id, &mut user_mutations, None) + let (validation, user) = User::mutate_by_id(&user_id, &mut user_mutations, None) .await .extract(&req)?; if !validation.is_success() { @@ -45,7 +45,7 @@ pub async fn logout(req: Request) -> Result { let mut mutations = Map::from_entry("status", "SignedOut"); let user_id = user_session.user_id(); - let (validation, user) = User::update_by_id(user_id, &mut mutations, None) + let (validation, user) = User::mutate_by_id(user_id, &mut mutations, None) .await .extract(&req)?; if !validation.is_success() { diff --git a/examples/axum-app/src/controller/auth.rs b/examples/axum-app/src/controller/auth.rs index fa05204d..ffd8d2e8 100644 --- a/examples/axum-app/src/controller/auth.rs +++ b/examples/axum-app/src/controller/auth.rs @@ -1,28 +1,28 @@ -use crate::model::User; +use crate::model::{User, UserColumn::*}; use zino::{prelude::*, Request, Response, Result}; use zino_model::user::JwtAuthService; pub async fn login(mut req: Request) -> Result { - let current_time = DateTime::now(); let body: Map = req.parse_body().await?; let (user_id, mut data) = User::generate_token(body).await.extract(&req)?; - let user_updates = json!({ - "status": "Active", - "last_login_at": data.remove("current_login_at").and_then(|v| v.as_date_time()), - "last_login_ip": data.remove("current_login_ip"), - "current_login_at": current_time, - "current_login_ip": req.client_ip(), - "$inc": { "login_count": 1 }, - }); - - let mut user_mutations = user_updates.into_map_opt().unwrap_or_default(); - let (validation, user) = User::update_by_id(&user_id, &mut user_mutations, None) + let last_login_ip = data.remove("current_login_ip"); + let last_login_at = data + .remove("current_login_at") + .and_then(|v| v.as_date_time()); + let mut mutation = MutationBuilder::::new() + .set(Status, "Active") + .set_if_not_null(LastLoginIp, last_login_ip) + .set_if_some(LastLoginAt, last_login_at) + .set_if_some(CurrentLoginIp, req.client_ip()) + .set_now(CurrentLoginAt) + .inc_one(LoginCount) + .set_now(UpdatedAt) + .inc_one(Version) + .build(); + let user: User = User::update_by_id(&user_id, &mut mutation) .await .extract(&req)?; - if !validation.is_success() { - reject!(req, validation); - } data.upsert("entry", user.snapshot()); let mut res = Response::default().context(&req); @@ -41,20 +41,20 @@ pub async fn refresh(req: Request) -> Result { pub async fn logout(req: Request) -> Result { let user_session = req .get_data::>() - .ok_or_else(|| warn!("401 Unauthorized: the user session is invalid")) + .ok_or_else(|| warn!("401 Unauthorized: user session is invalid")) .extract(&req)?; - - let mut mutations = Map::from_entry("status", "SignedOut"); let user_id = user_session.user_id(); - let (validation, user) = User::update_by_id(user_id, &mut mutations, None) + + let mut mutation = MutationBuilder::::new() + .set(Status, "SignedOut") + .set_now(UpdatedAt) + .inc_one(Version) + .build(); + let user: User = User::update_by_id(user_id, &mut mutation) .await .extract(&req)?; - if !validation.is_success() { - reject!(req, validation); - } - let data = Map::data_entry(user.snapshot()); let mut res = Response::default().context(&req); - res.set_json_data(data); + res.set_json_data(Map::data_entry(user.snapshot())); Ok(res.into()) } diff --git a/examples/ntex-app/src/controller/auth.rs b/examples/ntex-app/src/controller/auth.rs index 24839b54..71bc40aa 100644 --- a/examples/ntex-app/src/controller/auth.rs +++ b/examples/ntex-app/src/controller/auth.rs @@ -16,7 +16,7 @@ pub async fn login(mut req: Request) -> Result { }); let mut user_mutations = user_updates.into_map_opt().unwrap_or_default(); - let (validation, user) = User::update_by_id(&user_id, &mut user_mutations, None) + let (validation, user) = User::mutate_by_id(&user_id, &mut user_mutations, None) .await .extract(&req)?; if !validation.is_success() { @@ -45,7 +45,7 @@ pub async fn logout(req: Request) -> Result { let mut mutations = Map::from_entry("status", "SignedOut"); let user_id = user_session.user_id(); - let (validation, user) = User::update_by_id(user_id, &mut mutations, None) + let (validation, user) = User::mutate_by_id(user_id, &mut mutations, None) .await .extract(&req)?; if !validation.is_success() { diff --git a/zino-core/src/orm/accessor.rs b/zino-core/src/orm/accessor.rs index 907123be..661c2f58 100644 --- a/zino-core/src/orm/accessor.rs +++ b/zino-core/src/orm/accessor.rs @@ -407,8 +407,8 @@ where Ok(()) } - /// Updates a model of the primary key using the json object. - async fn update_by_id( + /// Mutates a model of the primary key with the JSON data and the optional extension. + async fn mutate_by_id( id: &K, data: &mut Map, extension: Option<::Extension>, diff --git a/zino-core/src/orm/mutation.rs b/zino-core/src/orm/mutation.rs index 5f66f720..94259021 100644 --- a/zino-core/src/orm/mutation.rs +++ b/zino-core/src/orm/mutation.rs @@ -63,6 +63,25 @@ impl MutationBuilder { self } + /// Sets the value of a column if the value is not null. + #[inline] + pub fn set_if_not_null(mut self, col: E::Column, value: impl IntoSqlValue) -> Self { + let value = value.into_sql_value(); + if !value.is_null() { + self.updates.upsert(col.as_ref(), value); + } + self + } + + /// Sets the value of a column if the value is not none. + #[inline] + pub fn set_if_some(mut self, col: E::Column, value: Option) -> Self { + if let Some(value) = value { + self.updates.upsert(col.as_ref(), value.into_sql_value()); + } + self + } + /// Sets the value of a column to the current date time. #[inline] pub fn set_now(mut self, col: E::Column) -> Self { diff --git a/zino-core/src/orm/mysql.rs b/zino-core/src/orm/mysql.rs index d03e75a9..277b76b7 100644 --- a/zino-core/src/orm/mysql.rs +++ b/zino-core/src/orm/mysql.rs @@ -197,7 +197,10 @@ impl<'c> EncodeColumn for Column<'c> { name } }; - if operator == "IN" || operator == "NOT IN" { + if let Some(subquery) = value.as_object().and_then(|m| m.get_str("$subquery")) { + let condition = format!(r#"{field} {operator} {subquery}"#); + conditions.push(condition); + } else if operator == "IN" || operator == "NOT IN" { if let Some(values) = value.as_array() { if values.is_empty() { let condition = if operator == "IN" { "FALSE" } else { "TRUE" }; @@ -627,12 +630,12 @@ impl QueryExt for Query { } else if field.contains('.') { field .split('.') - .map(|s| format!("`{s}`")) + .map(|s| ["`", s, "`"].concat()) .collect::>() .join(".") .into() } else { - format!("`{field}`").into() + ["`", field, "`"].concat().into() } } @@ -651,7 +654,7 @@ impl QueryExt for Query { } else if field.contains('.') { field .split('.') - .map(|s| format!("`{s}`")) + .map(|s| ["`", s, "`"].concat()) .collect::>() .join(".") } else { @@ -670,7 +673,7 @@ impl QueryExt for Query { if table_name.contains('.') { let table_name = table_name .split('.') - .map(|s| format!("`{s}`")) + .map(|s| ["`", s, "`"].concat()) .collect::>() .join("."); format!(r#"{table_name} AS `{model_name}`"#) @@ -684,18 +687,18 @@ impl QueryExt for Query { if table_name.contains('.') { table_name .split('.') - .map(|s| format!("`{s}`")) + .map(|s| ["`", s, "`"].concat()) .collect::>() .join(".") } else { - format!(r#"`{table_name}`"#) + ["`", table_name, "`"].concat() } } fn parse_text_search(filter: &Map) -> Option { let fields = filter.parse_str_array("$fields")?; filter.parse_string("$search").map(|search| { - let fields = fields.join(","); + let fields = fields.join(", "); let search = Query::escape_string(search.as_ref()); format!("match({fields}) against({search})") }) diff --git a/zino-core/src/orm/postgres.rs b/zino-core/src/orm/postgres.rs index db1f80c5..e89a1633 100644 --- a/zino-core/src/orm/postgres.rs +++ b/zino-core/src/orm/postgres.rs @@ -206,7 +206,10 @@ impl<'c> EncodeColumn for Column<'c> { name } }; - if operator == "IN" || operator == "NOT IN" { + if let Some(subquery) = value.as_object().and_then(|m| m.get_str("$subquery")) { + let condition = format!(r#"{field} {operator} {subquery}"#); + conditions.push(condition); + } else if operator == "IN" || operator == "NOT IN" { if let Some(values) = value.as_array() { if values.is_empty() { let condition = if operator == "IN" { "FALSE" } else { "TRUE" }; @@ -627,12 +630,12 @@ impl QueryExt for Query { } else if field.contains('.') { field .split('.') - .map(|s| format!(r#""{s}""#)) + .map(|s| ["\"", s, "\""].concat()) .collect::>() .join(".") .into() } else { - format!(r#""{field}""#).into() + ["\"", field, "\""].concat().into() } } @@ -651,7 +654,7 @@ impl QueryExt for Query { } else if field.contains('.') { field .split('.') - .map(|s| format!(r#""{s}""#)) + .map(|s| ["\"", s, "\""].concat()) .collect::>() .join(".") } else { @@ -670,7 +673,7 @@ impl QueryExt for Query { if table_name.contains('.') { let table_name = table_name .split('.') - .map(|s| format!(r#""{s}""#)) + .map(|s| ["\"", s, "\""].concat()) .collect::>() .join("."); format!(r#"{table_name} AS "{model_name}""#) @@ -684,11 +687,11 @@ impl QueryExt for Query { if table_name.contains('.') { table_name .split('.') - .map(|s| format!(r#""{s}""#)) + .map(|s| ["\"", s, "\""].concat()) .collect::>() .join(".") } else { - format!(r#""{table_name}""#) + ["\"", table_name, "\""].concat() } } diff --git a/zino-core/src/orm/query.rs b/zino-core/src/orm/query.rs index 1f4ec23f..2894a0d4 100644 --- a/zino-core/src/orm/query.rs +++ b/zino-core/src/orm/query.rs @@ -320,6 +320,17 @@ impl QueryBuilder { self } + /// Adds a logical `AND` condition for equal parts if the value is not null. + #[inline] + pub fn and_eq_if_not_null(mut self, col: E::Column, value: impl IntoSqlValue) -> Self { + let value = value.into_sql_value(); + if !value.is_null() { + let condition = Map::from_entry(E::format_column(&col), value); + self.logical_and.push(condition); + } + self + } + /// Adds a logical `AND` condition for equal parts if the value is not none. #[inline] pub fn and_eq_if_some(mut self, col: E::Column, value: Option) -> Self { @@ -336,6 +347,17 @@ impl QueryBuilder { self.push_logical_and(col, "$ne", value.into_sql_value()) } + /// Adds a logical `AND` condition for non-equal parts if the value is not null. + #[inline] + pub fn and_ne_if_not_null(self, col: E::Column, value: impl IntoSqlValue) -> Self { + let value = value.into_sql_value(); + if !value.is_null() { + self.push_logical_and(col, "$ne", value) + } else { + self + } + } + /// Adds a logical `AND` condition for non-equal parts if the value is not none. #[inline] pub fn and_ne_if_some(self, col: E::Column, value: Option) -> Self { @@ -390,6 +412,48 @@ impl QueryBuilder { self.push_logical_and(col, "$nin", values.into().into_sql_value()) } + /// Adds a logical `AND` condition for the columns `IN` a subquery. + pub fn and_in_subquery(mut self, cols: C, subquery: QueryBuilder) -> Self + where + C: Into>, + M: Entity + Schema, + { + let cols = cols + .into() + .into_iter() + .map(|col| { + let col_name = E::format_column(&col); + Query::format_field(&col_name).into_owned() + }) + .collect::>() + .join(", "); + let field = format!("({cols})"); + let condition = Map::from_entry("$in", subquery.into_sql_value()); + self.logical_and.push(Map::from_entry(field, condition)); + self + } + + /// Adds a logical `AND` condition for the columns `NOT IN` a subquery. + pub fn and_not_in_subquery(mut self, cols: C, subquery: QueryBuilder) -> Self + where + C: Into>, + M: Entity + Schema, + { + let cols = cols + .into() + .into_iter() + .map(|col| { + let col_name = E::format_column(&col); + Query::format_field(&col_name).into_owned() + }) + .collect::>() + .join(", "); + let field = format!("({cols})"); + let condition = Map::from_entry("$nin", subquery.into_sql_value()); + self.logical_and.push(Map::from_entry(field, condition)); + self + } + /// Adds a logical `AND` condition for the column in a range `[min, max)`. pub fn and_in_range(mut self, col: E::Column, min: T, max: T) -> Self { let field = E::format_column(&col); @@ -514,6 +578,17 @@ impl QueryBuilder { self } + /// Adds a logical `OR` condition for equal parts if the value is not null. + #[inline] + pub fn or_eq_if_not_null(mut self, col: E::Column, value: impl IntoSqlValue) -> Self { + let value = value.into_sql_value(); + if !value.is_null() { + let condition = Map::from_entry(E::format_column(&col), value); + self.logical_or.push(condition); + } + self + } + /// Adds a logical `OR` condition for equal parts if the value is not none. #[inline] pub fn or_eq_if_some(mut self, col: E::Column, value: Option) -> Self { @@ -530,6 +605,17 @@ impl QueryBuilder { self.push_logical_or(col, "$ne", value.into_sql_value()) } + /// Adds a logical `OR` condition for non-equal parts if the value is not none. + #[inline] + pub fn or_ne_if_not_null(self, col: E::Column, value: impl IntoSqlValue) -> Self { + let value = value.into_sql_value(); + if !value.is_null() { + self.push_logical_or(col, "$ne", value) + } else { + self + } + } + /// Adds a logical `OR` condition for non-equal parts if the value is not none. #[inline] pub fn or_ne_if_some(self, col: E::Column, value: Option) -> Self { @@ -584,6 +670,48 @@ impl QueryBuilder { self.push_logical_or(col, "$nin", values.into().into_sql_value()) } + /// Adds a logical `OR` condition for the columns `IN` a subquery. + pub fn or_in_subquery(mut self, cols: C, subquery: QueryBuilder) -> Self + where + C: Into>, + M: Entity + Schema, + { + let cols = cols + .into() + .into_iter() + .map(|col| { + let col_name = E::format_column(&col); + Query::format_field(&col_name).into_owned() + }) + .collect::>() + .join(", "); + let field = format!("({cols})"); + let condition = Map::from_entry("$in", subquery.into_sql_value()); + self.logical_or.push(Map::from_entry(field, condition)); + self + } + + /// Adds a logical `OR` condition for the columns `NOT IN` a subquery. + pub fn or_not_in_subquery(mut self, cols: C, subquery: QueryBuilder) -> Self + where + C: Into>, + M: Entity + Schema, + { + let cols = cols + .into() + .into_iter() + .map(|col| { + let col_name = E::format_column(&col); + Query::format_field(&col_name).into_owned() + }) + .collect::>() + .join(", "); + let field = format!("({cols})"); + let condition = Map::from_entry("$nin", subquery.into_sql_value()); + self.logical_or.push(Map::from_entry(field, condition)); + self + } + /// Adds a logical `OR` condition for the column is in a range `[min, max)`. pub fn or_in_range(mut self, col: E::Column, min: T, max: T) -> Self { let field = E::format_column(&col); @@ -1045,10 +1173,16 @@ pub(super) trait QueryExt { "$le" => "<=", "$gt" => ">", "$ge" => ">=", + "$in" => "IN", + "$nin" => "NOT IN", _ => "=", }; let field = Self::format_field(key); - let condition = if let Some(s) = value.as_str() { + let condition = if let Some(subquery) = + value.as_object().and_then(|m| m.get_str("$subquery")) + { + format!(r#"{field} {operator} {subquery}"#) + } else if let Some(s) = value.as_str() { if name == "$subquery" { format!(r#"{field} {operator} {s}"#) } else { diff --git a/zino-core/src/orm/scalar.rs b/zino-core/src/orm/scalar.rs index 8c5c60f9..4f79160a 100644 --- a/zino-core/src/orm/scalar.rs +++ b/zino-core/src/orm/scalar.rs @@ -166,13 +166,14 @@ where /// Finds a model selected by the primary key in the table, /// and decodes the column value as a single concrete type `T`. - async fn find_scalar_by_id(primary_key: &Self::PrimaryKey, column: &str) -> Result + async fn find_scalar_by_id(primary_key: &Self::PrimaryKey, column: C) -> Result where + C: AsRef, T: Send + Unpin + Type + for<'r> Decode<'r, DatabaseDriver>, { let primary_key_name = Self::PRIMARY_KEY_NAME; let table_name = Query::table_name_escaped::(); - let projection = Query::format_field(column); + let projection = Query::format_field(column.as_ref()); let placeholder = Query::placeholder(1); let sql = if cfg!(feature = "orm-postgres") { let type_annotation = Self::primary_key_column().type_annotation(); diff --git a/zino-core/src/orm/schema.rs b/zino-core/src/orm/schema.rs index 785aed68..afc0ce11 100644 --- a/zino-core/src/orm/schema.rs +++ b/zino-core/src/orm/schema.rs @@ -10,6 +10,7 @@ use crate::{ warn, JsonValue, Map, }; use serde::de::DeserializeOwned; +use sqlx::Acquire; use std::{fmt::Display, sync::atomic::Ordering::Relaxed}; /// Database schema. @@ -1640,6 +1641,92 @@ pub trait Schema: 'static + Send + Sync + ModelHooks { } } + /// Prepares the SQL to update a model selected by the primary key in the table. + async fn prepare_update_by_id(mutation: &mut Mutation) -> Result { + let primary_key_name = Self::PRIMARY_KEY_NAME; + let table_name = Query::table_name_escaped::(); + let updates = mutation.format_updates::(); + let placeholder = Query::placeholder(1); + let sql = if cfg!(any( + feature = "orm-mariadb", + feature = "orm-mysql", + feature = "orm-tidb" + )) { + format!( + "UPDATE {table_name} SET {updates} \ + WHERE {primary_key_name} = {placeholder};" + ) + } else if cfg!(feature = "orm-postgres") { + let type_annotation = Self::primary_key_column().type_annotation(); + format!( + "UPDATE {table_name} SET {updates} \ + WHERE {primary_key_name} = ({placeholder}){type_annotation} RETURNING *;" + ) + } else { + format!( + "UPDATE {table_name} SET {updates} \ + WHERE {primary_key_name} = {placeholder} RETURNING *;" + ) + }; + let mut ctx = Self::before_scan(&sql).await?; + ctx.set_query(sql); + if cfg!(debug_assertions) && super::DEBUG_ONLY.load(Relaxed) { + ctx.cancel(); + } + Ok(ctx) + } + + /// Updates a model selected by the primary key in the table, + /// and decodes it as an instance of type `T`. + async fn update_by_id( + primary_key: &Self::PrimaryKey, + mutation: &mut Mutation, + ) -> Result, Error> + where + T: DecodeRow, + { + let mut ctx = Self::prepare_update_by_id(mutation).await?; + if ctx.is_cancelled() { + return Ok(None); + } + + let pool = Self::acquire_writer().await?.pool(); + let optional_row = if cfg!(any( + feature = "orm-mariadb", + feature = "orm-mysql", + feature = "orm-tidb" + )) { + let mut transaction = pool.begin().await?; + let connection = transaction.acquire().await?; + let query_result = connection.execute_with(ctx.query(), &[primary_key]).await?; + let optional_row = if query_result.rows_affected() == 1 { + let primary_key_name = Self::PRIMARY_KEY_NAME; + let table_name = Query::table_name_escaped::(); + let placeholder = Query::placeholder(1); + let sql = + format!("SELECT * FROM {table_name} WHERE {primary_key_name} = {placeholder};"); + connection.fetch_optional_with(&sql, &[primary_key]).await? + } else { + None + }; + transaction.commit().await?; + optional_row + } else { + pool.fetch_optional_with(ctx.query(), &[primary_key]) + .await? + }; + let (num_rows, data) = if let Some(row) = optional_row { + (1, Some(T::decode_row(&row)?)) + } else { + (0, None) + }; + ctx.add_argument(primary_key); + ctx.set_query_result(num_rows, true); + Self::after_scan(&ctx).await?; + Self::after_query(&ctx).await?; + Ok(data) + } + /// Finds a model selected by the primary key in the table, /// and decodes it as an instance of type `T`. async fn find_by_id(primary_key: &Self::PrimaryKey) -> Result, Error> diff --git a/zino-core/src/orm/sqlite.rs b/zino-core/src/orm/sqlite.rs index 8f128f3e..88a1c47c 100644 --- a/zino-core/src/orm/sqlite.rs +++ b/zino-core/src/orm/sqlite.rs @@ -143,11 +143,7 @@ impl<'c> EncodeColumn for Column<'c> { format!(r#"json_tree.key = {key} AND json_tree.value = {value}"#); conditions.push(condition); } - if conditions.is_empty() { - return String::new(); - } else { - return format!("({})", conditions.join(" OR ")); - } + return Query::join_conditions(conditions, " OR "); } else { for (name, value) in filter { let name = name.as_str(); @@ -173,7 +169,10 @@ impl<'c> EncodeColumn for Column<'c> { name } }; - if operator == "IN" || operator == "NOT IN" { + if let Some(subquery) = value.as_object().and_then(|m| m.get_str("$subquery")) { + let condition = format!(r#"{field} {operator} {subquery}"#); + conditions.push(condition); + } else if operator == "IN" || operator == "NOT IN" { if let Some(values) = value.as_array() { if values.is_empty() { let condition = if operator == "IN" { "FALSE" } else { "TRUE" }; @@ -574,12 +573,12 @@ impl QueryExt for Query { } else if field.contains('.') { field .split('.') - .map(|s| format!("`{s}`")) + .map(|s| ["`", s, "`"].concat()) .collect::>() .join(".") .into() } else { - format!("`{field}`").into() + ["`", field, "`"].concat().into() } } @@ -598,7 +597,7 @@ impl QueryExt for Query { } else if field.contains('.') { field .split('.') - .map(|s| format!("`{s}`")) + .map(|s| ["`", s, "`"].concat()) .collect::>() .join(".") } else { @@ -637,11 +636,11 @@ impl QueryExt for Query { let table_name = if table_name.contains('.') { table_name .split('.') - .map(|s| format!("`{s}`")) + .map(|s| ["`", s, "`"].concat()) .collect::>() .join(".") } else { - format!(r#"`{table_name}`"#) + ["`", table_name, "`"].concat() }; if virtual_tables.is_empty() { format!(r#"{table_name} AS `{model_name}`"#) @@ -658,18 +657,18 @@ impl QueryExt for Query { if table_name.contains('.') { table_name .split('.') - .map(|s| format!("`{s}`")) + .map(|s| ["`", s, "`"].concat()) .collect::>() .join(".") } else { - format!(r#"`{table_name}`"#) + ["`", table_name, "`"].concat() } } fn parse_text_search(filter: &Map) -> Option { let fields = filter.parse_str_array("$fields")?; filter.parse_string("$search").map(|search| { - let fields = fields.join(","); + let fields = fields.join(", "); let search = Query::escape_string(search.as_ref()); format!("{fields} MATCH {search}") }) diff --git a/zino/src/controller/mod.rs b/zino/src/controller/mod.rs index f4e14601..e620b11f 100644 --- a/zino/src/controller/mod.rs +++ b/zino/src/controller/mod.rs @@ -132,7 +132,7 @@ where let mut body = req.parse_body().await?; let extension = req.get_data::<::Extension>(); - let (validation, model) = Self::update_by_id(&id, &mut body, extension) + let (validation, model) = Self::mutate_by_id(&id, &mut body, extension) .await .extract(&req)?; let mut res = Response::from(validation).context(&req);