873 lines
26 KiB
Rust
873 lines
26 KiB
Rust
use chrono::prelude::*;
|
|
use itertools::Itertools;
|
|
|
|
use super::{Connection, DBError, Result};
|
|
use crate::data::model;
|
|
|
|
use common::ron_api::Difficulty;
|
|
|
|
impl Connection {
|
|
/// Returns all the recipe titles where recipe is written in the given language.
|
|
/// If a user_id is given, the language constraint is ignored for recipes owned by user_id.
|
|
pub async fn get_all_published_recipe_titles(
|
|
&self,
|
|
lang: &str,
|
|
user_id: Option<i64>,
|
|
) -> Result<Vec<(i64, String)>> {
|
|
if let Some(user_id) = user_id {
|
|
sqlx::query_as(
|
|
r#"
|
|
SELECT [id], [title]
|
|
FROM [Recipe]
|
|
WHERE [is_published] = true AND ([lang] = $1 OR [user_id] = $2)
|
|
ORDER BY [title] COLLATE NOCASE
|
|
"#,
|
|
)
|
|
.bind(lang)
|
|
.bind(user_id)
|
|
} else {
|
|
sqlx::query_as(
|
|
r#"
|
|
SELECT [id], [title]
|
|
FROM [Recipe]
|
|
WHERE [is_published] = true AND [lang] = $1
|
|
ORDER BY [title] COLLATE NOCASE
|
|
"#,
|
|
)
|
|
.bind(lang)
|
|
}
|
|
.fetch_all(&self.pool)
|
|
.await
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn get_all_unpublished_recipe_titles(
|
|
&self,
|
|
owned_by: i64,
|
|
) -> Result<Vec<(i64, String)>> {
|
|
sqlx::query_as(
|
|
r#"
|
|
SELECT [id], [title]
|
|
FROM [Recipe]
|
|
WHERE [is_published] = false AND [user_id] = $1
|
|
ORDER BY [title]
|
|
"#,
|
|
)
|
|
.bind(owned_by)
|
|
.fetch_all(&self.pool)
|
|
.await
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn can_edit_recipe(&self, user_id: i64, recipe_id: i64) -> Result<bool> {
|
|
sqlx::query_scalar(
|
|
r#"SELECT COUNT(*) = 1 FROM [Recipe] WHERE [id] = $1 AND [user_id] = $2"#,
|
|
)
|
|
.bind(recipe_id)
|
|
.bind(user_id)
|
|
.fetch_one(&self.pool)
|
|
.await
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn can_edit_recipe_group(&self, user_id: i64, group_id: i64) -> Result<bool> {
|
|
sqlx::query_scalar(
|
|
r#"
|
|
SELECT COUNT(*) = 1
|
|
FROM [Recipe]
|
|
INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
|
|
WHERE [Group].[id] = $1 AND [user_id] = $2
|
|
"#,
|
|
)
|
|
.bind(group_id)
|
|
.bind(user_id)
|
|
.fetch_one(&self.pool)
|
|
.await
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn can_edit_recipe_all_groups(
|
|
&self,
|
|
user_id: i64,
|
|
group_ids: &[i64],
|
|
) -> Result<bool> {
|
|
let params = (0..group_ids.len())
|
|
.map(|n| format!("${}", n + 2))
|
|
.join(", ");
|
|
let query_str = format!(
|
|
r#"
|
|
SELECT COUNT(*)
|
|
FROM [Recipe]
|
|
INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
|
|
WHERE [Group].[id] IN ({}) AND [user_id] = $1
|
|
"#,
|
|
params
|
|
);
|
|
|
|
let mut query = sqlx::query_scalar::<_, u64>(&query_str).bind(user_id);
|
|
for id in group_ids {
|
|
query = query.bind(id);
|
|
}
|
|
Ok(query.fetch_one(&self.pool).await? == group_ids.len() as u64)
|
|
}
|
|
|
|
pub async fn can_edit_recipe_step(&self, user_id: i64, step_id: i64) -> Result<bool> {
|
|
sqlx::query_scalar(
|
|
r#"
|
|
SELECT COUNT(*) = 1
|
|
FROM [Recipe]
|
|
INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
|
|
INNER JOIN [Step] ON [Step].[group_id] = [Group].[id]
|
|
WHERE [Step].[id] = $1 AND [user_id] = $2
|
|
"#,
|
|
)
|
|
.bind(step_id)
|
|
.bind(user_id)
|
|
.fetch_one(&self.pool)
|
|
.await
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn can_edit_recipe_all_steps(&self, user_id: i64, steps_ids: &[i64]) -> Result<bool> {
|
|
let params = (0..steps_ids.len())
|
|
.map(|n| format!("${}", n + 2))
|
|
.join(", ");
|
|
let query_str = format!(
|
|
r#"
|
|
SELECT COUNT(*)
|
|
FROM [Recipe]
|
|
INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
|
|
INNER JOIN [Step] ON [Step].[group_id] = [Group].[id]
|
|
WHERE [Step].[id] IN ({}) AND [user_id] = $1
|
|
"#,
|
|
params
|
|
);
|
|
|
|
let mut query = sqlx::query_scalar::<_, u64>(&query_str).bind(user_id);
|
|
for id in steps_ids {
|
|
query = query.bind(id);
|
|
}
|
|
Ok(query.fetch_one(&self.pool).await? == steps_ids.len() as u64)
|
|
}
|
|
|
|
pub async fn can_edit_recipe_ingredient(
|
|
&self,
|
|
user_id: i64,
|
|
ingredient_id: i64,
|
|
) -> Result<bool> {
|
|
sqlx::query_scalar(
|
|
r#"
|
|
SELECT COUNT(*)
|
|
FROM [Recipe]
|
|
INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
|
|
INNER JOIN [Step] ON [Step].[group_id] = [Group].[id]
|
|
INNER JOIN [Ingredient] ON [Ingredient].[step_id] = [Step].[id]
|
|
WHERE [Ingredient].[id] = $1 AND [user_id] = $2
|
|
"#,
|
|
)
|
|
.bind(ingredient_id)
|
|
.bind(user_id)
|
|
.fetch_one(&self.pool)
|
|
.await
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn can_edit_recipe_all_ingredients(
|
|
&self,
|
|
user_id: i64,
|
|
ingredients_ids: &[i64],
|
|
) -> Result<bool> {
|
|
let params = (0..ingredients_ids.len())
|
|
.map(|n| format!("${}", n + 2))
|
|
.join(", ");
|
|
let query_str = format!(
|
|
r#"
|
|
SELECT COUNT(*)
|
|
FROM [Recipe]
|
|
INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
|
|
INNER JOIN [Step] ON [Step].[group_id] = [Group].[id]
|
|
INNER JOIN [Ingredient] ON [Ingredient].[step_id] = [Step].[id]
|
|
WHERE [Ingredient].[id] IN ({}) AND [user_id] = $1
|
|
"#,
|
|
params
|
|
);
|
|
|
|
let mut query = sqlx::query_scalar::<_, u64>(&query_str).bind(user_id);
|
|
for id in ingredients_ids {
|
|
query = query.bind(id);
|
|
}
|
|
Ok(query.fetch_one(&self.pool).await? == ingredients_ids.len() as u64)
|
|
}
|
|
|
|
pub async fn get_recipe(&self, id: i64, complete: bool) -> Result<Option<model::Recipe>> {
|
|
match sqlx::query_as::<_, model::Recipe>(
|
|
r#"
|
|
SELECT
|
|
[id], [user_id], [title], [lang],
|
|
[estimated_time], [description], [difficulty], [servings],
|
|
[is_published]
|
|
FROM [Recipe] WHERE [id] = $1
|
|
"#,
|
|
)
|
|
.bind(id)
|
|
.fetch_optional(&self.pool)
|
|
.await?
|
|
{
|
|
Some(mut recipe) if complete => {
|
|
recipe.tags = self.get_recipes_tags(id).await?;
|
|
recipe.groups = self.get_groups(id).await?;
|
|
Ok(Some(recipe))
|
|
}
|
|
recipe => Ok(recipe),
|
|
}
|
|
}
|
|
|
|
pub async fn create_recipe(&self, user_id: i64) -> Result<i64> {
|
|
let mut tx = self.tx().await?;
|
|
|
|
// Search for an existing empty recipe and return its id instead of creating a new one.
|
|
match sqlx::query_scalar::<_, i64>(
|
|
r#"
|
|
SELECT [Recipe].[id] FROM [Recipe]
|
|
LEFT JOIN [Image] ON [Image].[recipe_id] = [Recipe].[id]
|
|
LEFT JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
|
|
WHERE [Recipe].[user_id] = $1
|
|
AND [Recipe].[title] = ''
|
|
AND [Recipe].[estimated_time] IS NULL
|
|
AND [Recipe].[description] = ''
|
|
AND [Image].[id] IS NULL
|
|
AND [Group].[id] IS NULL
|
|
"#,
|
|
)
|
|
.bind(user_id)
|
|
.fetch_optional(&mut *tx)
|
|
.await?
|
|
{
|
|
Some(recipe_id) => Ok(recipe_id),
|
|
None => {
|
|
let lang: String = sqlx::query_scalar("SELECT [lang] FROM [User] WHERE [id] = $1")
|
|
.bind(user_id)
|
|
.fetch_one(&mut *tx)
|
|
.await?;
|
|
|
|
let db_result = sqlx::query(
|
|
"INSERT INTO [Recipe] ([user_id], [lang], [title], [creation_datetime]) VALUES ($1, $2, '', $3)",
|
|
)
|
|
.bind(user_id)
|
|
.bind(lang)
|
|
.bind(Utc::now())
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
tx.commit().await?;
|
|
Ok(db_result.last_insert_rowid())
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn set_recipe_title(&self, recipe_id: i64, title: &str) -> Result<()> {
|
|
sqlx::query("UPDATE [Recipe] SET [title] = $2 WHERE [id] = $1")
|
|
.bind(recipe_id)
|
|
.bind(title)
|
|
.execute(&self.pool)
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn set_recipe_description(&self, recipe_id: i64, description: &str) -> Result<()> {
|
|
sqlx::query("UPDATE [Recipe] SET [description] = $2 WHERE [id] = $1")
|
|
.bind(recipe_id)
|
|
.bind(description)
|
|
.execute(&self.pool)
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn set_recipe_servings(&self, recipe_id: i64, servings: Option<u32>) -> Result<()> {
|
|
sqlx::query("UPDATE [Recipe] SET [servings] = $2 WHERE [id] = $1")
|
|
.bind(recipe_id)
|
|
.bind(servings)
|
|
.execute(&self.pool)
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn set_recipe_estimated_time(
|
|
&self,
|
|
recipe_id: i64,
|
|
estimated_time: Option<u32>,
|
|
) -> Result<()> {
|
|
sqlx::query("UPDATE [Recipe] SET [estimated_time] = $2 WHERE [id] = $1")
|
|
.bind(recipe_id)
|
|
.bind(estimated_time)
|
|
.execute(&self.pool)
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn get_all_tags(&self) -> Result<Vec<String>> {
|
|
sqlx::query_scalar(
|
|
r#"
|
|
SELECT [name] FROM [Tag]
|
|
ORDER BY [name]
|
|
"#,
|
|
)
|
|
.fetch_all(&self.pool)
|
|
.await
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn get_all_tags_by_lang(&self, lang: &str) -> Result<Vec<String>> {
|
|
sqlx::query_scalar(
|
|
r#"
|
|
SELECT DISTINCT [name] FROM [Tag]
|
|
INNER JOIN [RecipeTag] ON [RecipeTag].[tag_id] = [Tag].[id]
|
|
INNER JOIN [Recipe] ON [Recipe].[id] = [RecipeTag].[recipe_id]
|
|
WHERE [Recipe].[lang] = $1
|
|
ORDER BY [name]
|
|
"#,
|
|
)
|
|
.bind(lang)
|
|
.fetch_all(&self.pool)
|
|
.await
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn get_recipes_tags(&self, recipe_id: i64) -> Result<Vec<String>> {
|
|
sqlx::query_scalar(
|
|
r#"
|
|
SELECT [name]
|
|
FROM [Tag]
|
|
INNER JOIN [RecipeTag] ON [RecipeTag].[tag_id] = [Tag].[id]
|
|
INNER JOIN [Recipe] ON [Recipe].[id] = [RecipeTag].[recipe_id]
|
|
WHERE [Recipe].[id] = $1
|
|
ORDER BY [name]
|
|
"#,
|
|
)
|
|
.bind(recipe_id)
|
|
.fetch_all(&self.pool)
|
|
.await
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn add_recipe_tags<T>(&self, recipe_id: i64, tags: &[T]) -> Result<()>
|
|
where
|
|
T: AsRef<str>,
|
|
{
|
|
let mut tx = self.tx().await?;
|
|
for tag in tags {
|
|
let tag = tag.as_ref().trim().to_lowercase();
|
|
let tag_id: i64 = if let Some(tag_id) =
|
|
sqlx::query_scalar("SELECT [id] FROM [Tag] WHERE [name] = $1")
|
|
.bind(&tag)
|
|
.fetch_optional(&mut *tx)
|
|
.await?
|
|
{
|
|
tag_id
|
|
} else {
|
|
let result = sqlx::query("INSERT INTO [Tag] ([name]) VALUES ($1)")
|
|
.bind(&tag)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
result.last_insert_rowid()
|
|
};
|
|
|
|
sqlx::query(
|
|
r#"
|
|
INSERT INTO [RecipeTag] ([recipe_id], [tag_id])
|
|
VALUES ($1, $2)
|
|
ON CONFLICT DO NOTHING
|
|
"#,
|
|
)
|
|
.bind(recipe_id)
|
|
.bind(tag_id)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
}
|
|
|
|
tx.commit().await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn rm_recipe_tags<T>(&self, recipe_id: i64, tags: &[T]) -> Result<()>
|
|
where
|
|
T: AsRef<str>,
|
|
{
|
|
let mut tx = self.tx().await?;
|
|
for tag in tags {
|
|
if let Some(tag_id) = sqlx::query_scalar::<_, i64>(
|
|
r#"
|
|
DELETE FROM [RecipeTag]
|
|
WHERE [id] IN (
|
|
SELECT [RecipeTag].[id] FROM [RecipeTag]
|
|
INNER JOIN [Tag] ON [Tag].[id] = [tag_id]
|
|
WHERE [recipe_id] = $1 AND [Tag].[name] = $2
|
|
)
|
|
RETURNING [RecipeTag].[tag_id]
|
|
"#,
|
|
)
|
|
.bind(recipe_id)
|
|
.bind(tag.as_ref())
|
|
.fetch_optional(&mut *tx)
|
|
.await?
|
|
{
|
|
sqlx::query(
|
|
r#"
|
|
DELETE FROM [Tag]
|
|
WHERE [id] = $1 AND [id] NOT IN (
|
|
SELECT [tag_id] FROM [RecipeTag]
|
|
WHERE [tag_id] = $1
|
|
)
|
|
"#,
|
|
)
|
|
.bind(tag_id)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
}
|
|
}
|
|
|
|
tx.commit().await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn set_recipe_difficulty(
|
|
&self,
|
|
recipe_id: i64,
|
|
difficulty: Difficulty,
|
|
) -> Result<()> {
|
|
sqlx::query("UPDATE [Recipe] SET [difficulty] = $2 WHERE [id] = $1")
|
|
.bind(recipe_id)
|
|
.bind(u32::from(difficulty))
|
|
.execute(&self.pool)
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn set_recipe_language(&self, recipe_id: i64, lang: &str) -> Result<()> {
|
|
sqlx::query("UPDATE [Recipe] SET [lang] = $2 WHERE [id] = $1")
|
|
.bind(recipe_id)
|
|
.bind(lang)
|
|
.execute(&self.pool)
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn set_recipe_is_published(&self, recipe_id: i64, is_published: bool) -> Result<()> {
|
|
sqlx::query("UPDATE [Recipe] SET [is_published] = $2 WHERE [id] = $1")
|
|
.bind(recipe_id)
|
|
.bind(is_published)
|
|
.execute(&self.pool)
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn rm_recipe(&self, recipe_id: i64) -> Result<()> {
|
|
sqlx::query("DELETE FROM [Recipe] WHERE [id] = $1")
|
|
.bind(recipe_id)
|
|
.execute(&self.pool)
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn get_groups(&self, recipe_id: i64) -> Result<Vec<model::Group>> {
|
|
let mut tx = self.tx().await?;
|
|
let mut groups: Vec<model::Group> = sqlx::query_as(
|
|
r#"
|
|
SELECT [id], [name], [comment]
|
|
FROM [Group]
|
|
WHERE [recipe_id] = $1
|
|
ORDER BY [order]
|
|
"#,
|
|
)
|
|
.bind(recipe_id)
|
|
.fetch_all(&mut *tx)
|
|
.await?;
|
|
|
|
for group in groups.iter_mut() {
|
|
group.steps = sqlx::query_as(
|
|
r#"
|
|
SELECT [id], [action]
|
|
FROM [Step]
|
|
WHERE [group_id] = $1
|
|
ORDER BY [order]
|
|
"#,
|
|
)
|
|
.bind(group.id)
|
|
.fetch_all(&mut *tx)
|
|
.await?;
|
|
|
|
for step in group.steps.iter_mut() {
|
|
step.ingredients = sqlx::query_as(
|
|
r#"
|
|
SELECT [id], [name], [comment], [quantity_value], [quantity_unit]
|
|
FROM [Ingredient]
|
|
WHERE [step_id] = $1
|
|
ORDER BY [order]
|
|
"#,
|
|
)
|
|
.bind(step.id)
|
|
.fetch_all(&mut *tx)
|
|
.await?;
|
|
}
|
|
}
|
|
|
|
Ok(groups)
|
|
}
|
|
|
|
pub async fn add_recipe_group(&self, recipe_id: i64) -> Result<i64> {
|
|
let mut tx = self.tx().await?;
|
|
|
|
let last_order = sqlx::query_scalar(
|
|
"SELECT [order] FROM [Group] WHERE [recipe_id] = $1 ORDER BY [order] DESC LIMIT 1",
|
|
)
|
|
.bind(recipe_id)
|
|
.fetch_optional(&mut *tx)
|
|
.await?
|
|
.unwrap_or(-1);
|
|
|
|
let db_result = sqlx::query("INSERT INTO [Group] ([recipe_id], [order]) VALUES ($1, $2)")
|
|
.bind(recipe_id)
|
|
.bind(last_order + 1)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
tx.commit().await?;
|
|
|
|
Ok(db_result.last_insert_rowid())
|
|
}
|
|
|
|
pub async fn rm_recipe_group(&self, group_id: i64) -> Result<()> {
|
|
sqlx::query("DELETE FROM [Group] WHERE [id] = $1")
|
|
.bind(group_id)
|
|
.execute(&self.pool)
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn set_group_name(&self, group_id: i64, name: &str) -> Result<()> {
|
|
sqlx::query("UPDATE [Group] SET [name] = $2 WHERE [id] = $1")
|
|
.bind(group_id)
|
|
.bind(name)
|
|
.execute(&self.pool)
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn set_group_comment(&self, group_id: i64, comment: &str) -> Result<()> {
|
|
sqlx::query("UPDATE [Group] SET [comment] = $2 WHERE [id] = $1")
|
|
.bind(group_id)
|
|
.bind(comment)
|
|
.execute(&self.pool)
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn set_groups_order(&self, group_ids: &[i64]) -> Result<()> {
|
|
let mut tx = self.tx().await?;
|
|
|
|
for (order, id) in group_ids.iter().enumerate() {
|
|
sqlx::query("UPDATE [Group] SET [order] = $2 WHERE [id] = $1")
|
|
.bind(id)
|
|
.bind(order as i64)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
}
|
|
|
|
tx.commit().await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn add_recipe_step(&self, group_id: i64) -> Result<i64> {
|
|
let mut tx = self.tx().await?;
|
|
|
|
let last_order = sqlx::query_scalar(
|
|
"SELECT [order] FROM [Step] WHERE [group_id] = $1 ORDER BY [order] DESC LIMIT 1",
|
|
)
|
|
.bind(group_id)
|
|
.fetch_optional(&mut *tx)
|
|
.await?
|
|
.unwrap_or(-1);
|
|
|
|
let db_result = sqlx::query("INSERT INTO [Step] ([group_id], [order]) VALUES ($1, $2)")
|
|
.bind(group_id)
|
|
.bind(last_order + 1)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
tx.commit().await?;
|
|
|
|
Ok(db_result.last_insert_rowid())
|
|
}
|
|
|
|
pub async fn rm_recipe_step(&self, step_id: i64) -> Result<()> {
|
|
sqlx::query("DELETE FROM [Step] WHERE [id] = $1")
|
|
.bind(step_id)
|
|
.execute(&self.pool)
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn set_step_action(&self, step_id: i64, action: &str) -> Result<()> {
|
|
sqlx::query("UPDATE [Step] SET [action] = $2 WHERE [id] = $1")
|
|
.bind(step_id)
|
|
.bind(action)
|
|
.execute(&self.pool)
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn set_steps_order(&self, step_ids: &[i64]) -> Result<()> {
|
|
let mut tx = self.tx().await?;
|
|
|
|
for (order, id) in step_ids.iter().enumerate() {
|
|
sqlx::query("UPDATE [Step] SET [order] = $2 WHERE [id] = $1")
|
|
.bind(id)
|
|
.bind(order as i64)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
}
|
|
|
|
tx.commit().await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn add_recipe_ingredient(&self, step_id: i64) -> Result<i64> {
|
|
let mut tx = self.tx().await?;
|
|
|
|
let last_order = sqlx::query_scalar(
|
|
"SELECT [order] FROM [Ingredient] WHERE [step_id] = $1 ORDER BY [order] DESC LIMIT 1",
|
|
)
|
|
.bind(step_id)
|
|
.fetch_optional(&mut *tx)
|
|
.await?
|
|
.unwrap_or(-1);
|
|
|
|
let db_result = sqlx::query(
|
|
r#"
|
|
INSERT INTO [Ingredient] ([step_id], [order])
|
|
VALUES ($1, $2)
|
|
"#,
|
|
)
|
|
.bind(step_id)
|
|
.bind(last_order as i64)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
Ok(db_result.last_insert_rowid())
|
|
}
|
|
|
|
pub async fn rm_recipe_ingredient(&self, ingredient_id: i64) -> Result<()> {
|
|
sqlx::query("DELETE FROM [Ingredient] WHERE [id] = $1")
|
|
.bind(ingredient_id)
|
|
.execute(&self.pool)
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn set_ingredient_name(&self, ingredient_id: i64, name: &str) -> Result<()> {
|
|
sqlx::query("UPDATE [Ingredient] SET [name] = $2 WHERE [id] = $1")
|
|
.bind(ingredient_id)
|
|
.bind(name)
|
|
.execute(&self.pool)
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn set_ingredient_comment(&self, ingredient_id: i64, comment: &str) -> Result<()> {
|
|
sqlx::query("UPDATE [Ingredient] SET [comment] = $2 WHERE [id] = $1")
|
|
.bind(ingredient_id)
|
|
.bind(comment)
|
|
.execute(&self.pool)
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn set_ingredient_quantity(
|
|
&self,
|
|
ingredient_id: i64,
|
|
quantity: Option<f64>,
|
|
) -> Result<()> {
|
|
sqlx::query("UPDATE [Ingredient] SET [quantity_value] = $2 WHERE [id] = $1")
|
|
.bind(ingredient_id)
|
|
.bind(quantity)
|
|
.execute(&self.pool)
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn set_ingredient_unit(&self, ingredient_id: i64, unit: &str) -> Result<()> {
|
|
sqlx::query("UPDATE [Ingredient] SET [quantity_unit] = $2 WHERE [id] = $1")
|
|
.bind(ingredient_id)
|
|
.bind(unit)
|
|
.execute(&self.pool)
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(DBError::from)
|
|
}
|
|
|
|
pub async fn set_ingredients_order(&self, ingredient_ids: &[i64]) -> Result<()> {
|
|
let mut tx = self.tx().await?;
|
|
|
|
for (order, id) in ingredient_ids.iter().enumerate() {
|
|
sqlx::query("UPDATE [Ingredient] SET [order] = $2 WHERE [id] = $1")
|
|
.bind(id)
|
|
.bind(order as i64)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
}
|
|
|
|
tx.commit().await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn create_a_new_recipe_then_update_its_title() -> Result<()> {
|
|
let connection = Connection::new_in_memory().await?;
|
|
|
|
let user_id = create_a_user(&connection).await?;
|
|
let recipe_id = connection.create_recipe(user_id).await?;
|
|
|
|
connection.set_recipe_title(recipe_id, "Crêpe").await?;
|
|
let recipe = connection.get_recipe(recipe_id, false).await?.unwrap();
|
|
assert_eq!(recipe.title, "Crêpe".to_string());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn setters() -> Result<()> {
|
|
let connection = Connection::new_in_memory().await?;
|
|
|
|
let user_id = create_a_user(&connection).await?;
|
|
let recipe_id = connection.create_recipe(user_id).await?;
|
|
|
|
connection.set_recipe_title(recipe_id, "Ouiche").await?;
|
|
connection
|
|
.set_recipe_description(recipe_id, "C'est bon, mangez-en")
|
|
.await?;
|
|
connection
|
|
.set_recipe_estimated_time(recipe_id, Some(420))
|
|
.await?;
|
|
connection
|
|
.set_recipe_difficulty(recipe_id, Difficulty::Medium)
|
|
.await?;
|
|
connection.set_recipe_language(recipe_id, "fr").await?;
|
|
connection.set_recipe_is_published(recipe_id, true).await?;
|
|
|
|
let recipe = connection.get_recipe(recipe_id, false).await?.unwrap();
|
|
|
|
assert_eq!(recipe.id, recipe_id);
|
|
assert_eq!(recipe.title, "Ouiche");
|
|
assert_eq!(recipe.description, "C'est bon, mangez-en");
|
|
assert_eq!(recipe.estimated_time, Some(420));
|
|
assert_eq!(recipe.difficulty, Difficulty::Medium);
|
|
assert_eq!(recipe.lang, "fr");
|
|
assert!(recipe.is_published);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn create_a_user(connection: &Connection) -> Result<i64> {
|
|
let user_id = 1;
|
|
connection.execute_sql(
|
|
sqlx::query(
|
|
r#"
|
|
INSERT INTO [User]
|
|
([id], [email], [name], [creation_datetime], [password], [validation_token_datetime], [validation_token])
|
|
VALUES
|
|
($1, $2, $3, $4, $5, $6, $7)
|
|
"#
|
|
)
|
|
.bind(user_id)
|
|
.bind("paul@atreides.com")
|
|
.bind("paul")
|
|
.bind("")
|
|
.bind("$argon2id$v=19$m=4096,t=3,p=1$G4fjepS05MkRbTqEImUdYg$GGziE8uVQe1L1oFHk37lBno10g4VISnVqynSkLCH3Lc")
|
|
.bind("2022-11-29 22:05:04.121407300+00:00")
|
|
.bind(None::<&str>) // 'null'.
|
|
).await?;
|
|
Ok(user_id)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn add_and_remove_tags() -> Result<()> {
|
|
let connection = Connection::new_in_memory().await?;
|
|
let user_id = create_a_user(&connection).await?;
|
|
let recipe_id_1 = connection.create_recipe(user_id).await?;
|
|
connection.set_recipe_title(recipe_id_1, "recipe 1").await?;
|
|
|
|
let tags_1 = ["abc", "xyz"];
|
|
connection.add_recipe_tags(recipe_id_1, &tags_1).await?;
|
|
|
|
// Adding the same tags should do nothing.
|
|
connection.add_recipe_tags(recipe_id_1, &tags_1).await?;
|
|
|
|
assert_eq!(connection.get_recipes_tags(recipe_id_1).await?, tags_1);
|
|
|
|
let tags_2 = ["abc", "def", "xyz"];
|
|
let recipe_id_2 = connection.create_recipe(user_id).await?;
|
|
connection.set_recipe_title(recipe_id_2, "recipe 2").await?;
|
|
|
|
connection.add_recipe_tags(recipe_id_2, &tags_2).await?;
|
|
|
|
assert_eq!(connection.get_recipes_tags(recipe_id_1).await?, tags_1);
|
|
assert_eq!(connection.get_recipes_tags(recipe_id_2).await?, tags_2);
|
|
|
|
assert_eq!(connection.get_all_tags().await?, ["abc", "def", "xyz"]);
|
|
connection.rm_recipe_tags(recipe_id_2, &["abc"]).await?;
|
|
assert_eq!(connection.get_all_tags().await?, ["abc", "def", "xyz"]);
|
|
|
|
assert_eq!(
|
|
connection.get_recipes_tags(recipe_id_1).await?,
|
|
["abc", "xyz"]
|
|
);
|
|
assert_eq!(
|
|
connection.get_recipes_tags(recipe_id_2).await?,
|
|
["def", "xyz"]
|
|
);
|
|
|
|
connection.rm_recipe_tags(recipe_id_1, &["abc"]).await?;
|
|
|
|
assert_eq!(connection.get_recipes_tags(recipe_id_1).await?, ["xyz"]);
|
|
assert_eq!(
|
|
connection.get_recipes_tags(recipe_id_2).await?,
|
|
["def", "xyz"]
|
|
);
|
|
assert_eq!(connection.get_all_tags().await?, ["def", "xyz"]);
|
|
assert_eq!(connection.get_all_tags_by_lang("en").await?, ["def", "xyz"]);
|
|
|
|
connection.rm_recipe_tags(recipe_id_1, &tags_1).await?;
|
|
connection.rm_recipe_tags(recipe_id_2, &tags_2).await?;
|
|
|
|
assert!(connection.get_all_tags().await?.is_empty());
|
|
|
|
Ok(())
|
|
}
|
|
}
|