From b31e551134a92f66d7bd93328971f9ef8758975a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Gori=C4=8Dar?= Date: Wed, 4 Sep 2024 13:33:25 +0200 Subject: [PATCH] feat: rework permisssion, role, and user entities in kolomoni_database, remove unused CLI flags in kolomoni_migrations, update dependencies, scripts, and makefile --- ...5e1c5ae4d25ce6393505bd3968bc7edc3d636.json | 22 ++ ...628ce84abe4257e114366d1c4dd148a7325fe.json | 22 ++ ...126925f17e9d2a12b084cac6257dab820991e.json | 58 ++++ ...1596fa652ce79457e26f8c2663c5cbb898720.json | 22 ++ ...5a5ecfbc022471eab83e24dab15f2eff6b375.json | 58 ++++ ...61188e76c3dfe0a7702023c69a7e487e487d1.json | 56 ++++ Cargo.lock | 7 +- Cargo.toml | 8 +- Makefile.toml | 57 +--- kolomoni_auth/Cargo.toml | 12 +- kolomoni_auth/src/hasher.rs | 54 ++++ kolomoni_auth/src/lib.rs | 2 + kolomoni_database/Cargo.toml | 14 +- kolomoni_database/src/entities/category.rs | 85 ----- kolomoni_database/src/entities/mod.rs | 13 - kolomoni_database/src/entities/permission.rs | 79 ----- .../src/entities/permission/mod.rs | 7 + .../src/entities/permission/model.rs | 19 ++ .../src/entities/permission/mutation.rs | 0 .../src/entities/permission/query.rs | 1 + kolomoni_database/src/entities/prelude.rs | 14 - kolomoni_database/src/entities/role.rs | 96 ------ kolomoni_database/src/entities/role/mod.rs | 2 + kolomoni_database/src/entities/role/model.rs | 15 + .../src/entities/role_permission.rs | 82 ----- kolomoni_database/src/entities/user.rs | 91 ------ kolomoni_database/src/entities/user/mod.rs | 4 + kolomoni_database/src/entities/user/model.rs | 73 +++++ kolomoni_database/src/entities/user/query.rs | 211 +++++++++++++ kolomoni_database/src/entities/user_role.rs | 82 ----- kolomoni_database/src/entities/word.rs | 92 ------ .../src/entities/word_category.rs | 82 ----- .../src/entities/word_english.rs | 100 ------ .../src/entities/word_slovene.rs | 100 ------ .../src/entities/word_translation.rs | 85 ----- .../entities/word_translation_suggestion.rs | 85 ----- kolomoni_database/src/impls.rs | 1 - kolomoni_database/src/impls/word.rs | 16 - kolomoni_database/src/lib.rs | 50 ++- kolomoni_database/src/macros.rs | 26 -- kolomoni_database/src/mutation.rs | 19 -- kolomoni_database/src/mutation/category.rs | 101 ------ kolomoni_database/src/mutation/user.rs | 241 --------------- kolomoni_database/src/mutation/user_role.rs | 89 ------ kolomoni_database/src/mutation/word.rs | 32 -- .../src/mutation/word_category.rs | 48 --- .../src/mutation/word_english.rs | 140 --------- .../src/mutation/word_slovene.rs | 139 --------- .../src/mutation/word_translation.rs | 116 ------- .../mutation/word_translation_suggestion.rs | 118 ------- kolomoni_database/src/query.rs | 19 -- kolomoni_database/src/query/category.rs | 125 -------- kolomoni_database/src/query/user.rs | 139 --------- kolomoni_database/src/query/user_role.rs | 177 ----------- kolomoni_database/src/query/word.rs | 57 ---- kolomoni_database/src/query/word_category.rs | 70 ----- kolomoni_database/src/query/word_english.rs | 292 ------------------ kolomoni_database/src/query/word_slovene.rs | 238 -------------- .../src/query/word_translation.rs | 69 ----- .../src/query/word_translation_suggestion.rs | 69 ----- kolomoni_database/src/shared.rs | 39 --- .../migrations/M0002_set-up-tables/up.sql | 42 +-- .../M0003_seed-permissions-and-roles/down.rs | 4 +- .../permissions.rs | 76 +++-- .../M0003_seed-permissions-and-roles/roles.rs | 29 +- .../M0003_seed-permissions-and-roles/up.rs | 26 +- kolomoni_migrations/src/cli.rs | 20 +- scripts/database/init-database.ps1 | 14 +- 68 files changed, 792 insertions(+), 3659 deletions(-) create mode 100644 .sqlx/query-02dd46fcf0c17225772661cfeea5e1c5ae4d25ce6393505bd3968bc7edc3d636.json create mode 100644 .sqlx/query-0b4ce40283d4c1cfb9c8fc34cd6628ce84abe4257e114366d1c4dd148a7325fe.json create mode 100644 .sqlx/query-7cc7b90ce168a29146e843d9e37126925f17e9d2a12b084cac6257dab820991e.json create mode 100644 .sqlx/query-85b9529ad42b39a40ed1c8ccf5a1596fa652ce79457e26f8c2663c5cbb898720.json create mode 100644 .sqlx/query-8ad7238e89a91b7ab75c00d4bca5a5ecfbc022471eab83e24dab15f2eff6b375.json create mode 100644 .sqlx/query-fb449c6119103e581753aebab9561188e76c3dfe0a7702023c69a7e487e487d1.json create mode 100644 kolomoni_auth/src/hasher.rs delete mode 100644 kolomoni_database/src/entities/category.rs delete mode 100644 kolomoni_database/src/entities/permission.rs create mode 100644 kolomoni_database/src/entities/permission/mod.rs create mode 100644 kolomoni_database/src/entities/permission/model.rs create mode 100644 kolomoni_database/src/entities/permission/mutation.rs create mode 100644 kolomoni_database/src/entities/permission/query.rs delete mode 100644 kolomoni_database/src/entities/prelude.rs delete mode 100644 kolomoni_database/src/entities/role.rs create mode 100644 kolomoni_database/src/entities/role/mod.rs create mode 100644 kolomoni_database/src/entities/role/model.rs delete mode 100644 kolomoni_database/src/entities/role_permission.rs delete mode 100644 kolomoni_database/src/entities/user.rs create mode 100644 kolomoni_database/src/entities/user/mod.rs create mode 100644 kolomoni_database/src/entities/user/model.rs create mode 100644 kolomoni_database/src/entities/user/query.rs delete mode 100644 kolomoni_database/src/entities/user_role.rs delete mode 100644 kolomoni_database/src/entities/word.rs delete mode 100644 kolomoni_database/src/entities/word_category.rs delete mode 100644 kolomoni_database/src/entities/word_english.rs delete mode 100644 kolomoni_database/src/entities/word_slovene.rs delete mode 100644 kolomoni_database/src/entities/word_translation.rs delete mode 100644 kolomoni_database/src/entities/word_translation_suggestion.rs delete mode 100644 kolomoni_database/src/impls.rs delete mode 100644 kolomoni_database/src/impls/word.rs delete mode 100644 kolomoni_database/src/macros.rs delete mode 100644 kolomoni_database/src/mutation.rs delete mode 100644 kolomoni_database/src/mutation/category.rs delete mode 100644 kolomoni_database/src/mutation/user.rs delete mode 100644 kolomoni_database/src/mutation/user_role.rs delete mode 100644 kolomoni_database/src/mutation/word.rs delete mode 100644 kolomoni_database/src/mutation/word_category.rs delete mode 100644 kolomoni_database/src/mutation/word_english.rs delete mode 100644 kolomoni_database/src/mutation/word_slovene.rs delete mode 100644 kolomoni_database/src/mutation/word_translation.rs delete mode 100644 kolomoni_database/src/mutation/word_translation_suggestion.rs delete mode 100644 kolomoni_database/src/query.rs delete mode 100644 kolomoni_database/src/query/category.rs delete mode 100644 kolomoni_database/src/query/user.rs delete mode 100644 kolomoni_database/src/query/user_role.rs delete mode 100644 kolomoni_database/src/query/word.rs delete mode 100644 kolomoni_database/src/query/word_category.rs delete mode 100644 kolomoni_database/src/query/word_english.rs delete mode 100644 kolomoni_database/src/query/word_slovene.rs delete mode 100644 kolomoni_database/src/query/word_translation.rs delete mode 100644 kolomoni_database/src/query/word_translation_suggestion.rs delete mode 100644 kolomoni_database/src/shared.rs diff --git a/.sqlx/query-02dd46fcf0c17225772661cfeea5e1c5ae4d25ce6393505bd3968bc7edc3d636.json b/.sqlx/query-02dd46fcf0c17225772661cfeea5e1c5ae4d25ce6393505bd3968bc7edc3d636.json new file mode 100644 index 0000000..5a7a137 --- /dev/null +++ b/.sqlx/query-02dd46fcf0c17225772661cfeea5e1c5ae4d25ce6393505bd3968bc7edc3d636.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS (SELECT 1 FROM kolomoni.user WHERE display_name = $1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "02dd46fcf0c17225772661cfeea5e1c5ae4d25ce6393505bd3968bc7edc3d636" +} diff --git a/.sqlx/query-0b4ce40283d4c1cfb9c8fc34cd6628ce84abe4257e114366d1c4dd148a7325fe.json b/.sqlx/query-0b4ce40283d4c1cfb9c8fc34cd6628ce84abe4257e114366d1c4dd148a7325fe.json new file mode 100644 index 0000000..98e3ad1 --- /dev/null +++ b/.sqlx/query-0b4ce40283d4c1cfb9c8fc34cd6628ce84abe4257e114366d1c4dd148a7325fe.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS (SELECT 1 FROM kolomoni.user WHERE username = $1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "0b4ce40283d4c1cfb9c8fc34cd6628ce84abe4257e114366d1c4dd148a7325fe" +} diff --git a/.sqlx/query-7cc7b90ce168a29146e843d9e37126925f17e9d2a12b084cac6257dab820991e.json b/.sqlx/query-7cc7b90ce168a29146e843d9e37126925f17e9d2a12b084cac6257dab820991e.json new file mode 100644 index 0000000..5e2fa09 --- /dev/null +++ b/.sqlx/query-7cc7b90ce168a29146e843d9e37126925f17e9d2a12b084cac6257dab820991e.json @@ -0,0 +1,58 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, username, display_name, hashed_password, joined_at, last_modified_at, last_active_at FROM kolomoni.user WHERE username = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "username", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "display_name", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "hashed_password", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "joined_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "last_modified_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "last_active_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "7cc7b90ce168a29146e843d9e37126925f17e9d2a12b084cac6257dab820991e" +} diff --git a/.sqlx/query-85b9529ad42b39a40ed1c8ccf5a1596fa652ce79457e26f8c2663c5cbb898720.json b/.sqlx/query-85b9529ad42b39a40ed1c8ccf5a1596fa652ce79457e26f8c2663c5cbb898720.json new file mode 100644 index 0000000..a0dfdeb --- /dev/null +++ b/.sqlx/query-85b9529ad42b39a40ed1c8ccf5a1596fa652ce79457e26f8c2663c5cbb898720.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS (SELECT 1 FROM kolomoni.user WHERE id = $1)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "85b9529ad42b39a40ed1c8ccf5a1596fa652ce79457e26f8c2663c5cbb898720" +} diff --git a/.sqlx/query-8ad7238e89a91b7ab75c00d4bca5a5ecfbc022471eab83e24dab15f2eff6b375.json b/.sqlx/query-8ad7238e89a91b7ab75c00d4bca5a5ecfbc022471eab83e24dab15f2eff6b375.json new file mode 100644 index 0000000..5ab3690 --- /dev/null +++ b/.sqlx/query-8ad7238e89a91b7ab75c00d4bca5a5ecfbc022471eab83e24dab15f2eff6b375.json @@ -0,0 +1,58 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, username, display_name, hashed_password, joined_at, last_modified_at, last_active_at FROM kolomoni.user WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "username", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "display_name", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "hashed_password", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "joined_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "last_modified_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "last_active_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "8ad7238e89a91b7ab75c00d4bca5a5ecfbc022471eab83e24dab15f2eff6b375" +} diff --git a/.sqlx/query-fb449c6119103e581753aebab9561188e76c3dfe0a7702023c69a7e487e487d1.json b/.sqlx/query-fb449c6119103e581753aebab9561188e76c3dfe0a7702023c69a7e487e487d1.json new file mode 100644 index 0000000..0bea8c3 --- /dev/null +++ b/.sqlx/query-fb449c6119103e581753aebab9561188e76c3dfe0a7702023c69a7e487e487d1.json @@ -0,0 +1,56 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, username, display_name, hashed_password, joined_at, last_modified_at, last_active_at FROM kolomoni.user", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "username", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "display_name", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "hashed_password", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "joined_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "last_modified_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "last_active_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "fb449c6119103e581753aebab9561188e76c3dfe0a7702023c69a7e487e487d1" +} diff --git a/Cargo.lock b/Cargo.lock index 07e288c..6a6bd49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1432,6 +1432,7 @@ version = "0.1.0" dependencies = [ "actix-utils", "actix-web", + "argon2", "chrono", "jsonwebtoken", "miette", @@ -1459,9 +1460,11 @@ version = "0.1.0" dependencies = [ "argon2", "chrono", + "futures-core", + "futures-util", "kolomoni_auth", - "kolomoni_configuration", - "miette", + "pin-project-lite", + "sqlx", "thiserror", "uuid", ] diff --git a/Cargo.toml b/Cargo.toml index 5c9305d..f40db08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,8 @@ publish = false members = [ "kolomoni_auth", "kolomoni_configuration", - # "kolomoni_database", + "kolomoni_database", + # "kolomoni_database_old", "kolomoni_migrations", "kolomoni_migrations_core", "kolomoni_migrations_macros", @@ -73,7 +74,6 @@ http = "0.2.12" mime = "0.3.17" uuid = { version = "1.8.0", features = ["v7"] } httpdate = "1.0.3" -futures-util = "0.3.30" bytes = "1.6.0" dotenvy = "0.15.7" @@ -86,6 +86,10 @@ path-slash = "0.2.1" fs-more = "0.7.1" +futures-core = "0.3.30" +futures-util = "0.3.30" +pin-project-lite = "0.2.14" + [workspace.dependencies.sqlx] version = "0.8.0" diff --git a/Makefile.toml b/Makefile.toml index 4ab6a57..ad1895f 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -12,59 +12,6 @@ CARGO_MAKE_USE_WORKSPACE_PROFILE = false - -[tasks."migrations:create"] -clear = true -workspace = false -command = "sea-orm-cli" -args = [ - "migrate", - "generate", - "--universal-time", - "--migration-dir", - "./kolomoni_migrations", - "${@}" -] - - -[tasks."migrations"] -clear = true -workspace = false -command = "sea-orm-cli" -args = [ - "migrate", - "${@}", - "--migration-dir", - "./kolomoni_migrations" -] - -[tasks."migrations:up"] -clear = true -workspace = false -command = "sea-orm-cli" -args = [ - "migrate", - "up", - "--migration-dir", - "./kolomoni_migrations" -] - - - -[tasks."entities:generate"] -clear = true -workspace = false -command = "sea-orm-cli" -args = [ - "generate", - "entity", - "--output-dir", - "./kolomoni_database/src/entities", - "--expanded-format" -] - - - [tasks."database:initialize"] clear = true workspace = false @@ -116,6 +63,7 @@ args = [ "--open" ] + [tasks."backend:run"] clear = true workspace = true @@ -123,7 +71,7 @@ command = "cargo" args = ["run", "--release"] cwd = "." -[tasks."openapi-backend:run"] +[tasks."backend:openapi:run"] clear = true workspace = true command = "cargo" @@ -147,6 +95,7 @@ args = [ [tasks.documentation] clear = true workspace = true +alias = "doc" dependencies = [ "build-and-open-full-documentation", "build-and-watch-workspace-documentation-with-private-items" diff --git a/kolomoni_auth/Cargo.toml b/kolomoni_auth/Cargo.toml index 2daebe2..588bc58 100644 --- a/kolomoni_auth/Cargo.toml +++ b/kolomoni_auth/Cargo.toml @@ -8,13 +8,21 @@ publish = false [dependencies] tokio = { workspace = true } + +tracing = { workspace = true } + actix-web = { workspace = true } +actix-utils = { workspace = true } + # sea-orm = { workspace = true } miette = { workspace = true } -actix-utils = { workspace = true } -tracing = { workspace = true } + thiserror = { workspace = true } + chrono = { workspace = true } + jsonwebtoken = { workspace = true } +argon2 = { workspace = true } + serde = { workspace = true } serde_with = { workspace = true } diff --git a/kolomoni_auth/src/hasher.rs b/kolomoni_auth/src/hasher.rs new file mode 100644 index 0000000..3500e2f --- /dev/null +++ b/kolomoni_auth/src/hasher.rs @@ -0,0 +1,54 @@ +use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; +use thiserror::Error; + + +#[derive(Debug, Error)] +pub enum ArgonHasherError { + #[error("argon2 error: {}", .error)] + Argon2Error { error: argon2::password_hash::Error }, +} + + + +pub struct ArgonHasher { + salt_string: SaltString, + argon_hasher: Argon2<'static>, +} + +impl ArgonHasher { + pub fn new(base64_hash_salt: &str) -> Result { + let salt_string = SaltString::from_b64(base64_hash_salt) + .map_err(|error| ArgonHasherError::Argon2Error { error })?; + + let argon_hasher = Argon2::new( + argon2::Algorithm::Argon2id, + argon2::Version::V0x13, + argon2::Params::default(), + ); + + Ok(Self { + salt_string, + argon_hasher, + }) + } + + pub fn hash_password(&self, password: &str) -> Result { + self.argon_hasher + .hash_password(password.as_bytes(), &self.salt_string) + .map_err(|error| ArgonHasherError::Argon2Error { error }) + } + + pub fn verify_password_against_hash( + &self, + password: &str, + hashed_password: &str, + ) -> Result { + let hashed_password = PasswordHash::new(hashed_password) + .map_err(|error| ArgonHasherError::Argon2Error { error })?; + + Ok(self + .argon_hasher + .verify_password(password.as_bytes(), &hashed_password) + .is_ok()) + } +} diff --git a/kolomoni_auth/src/lib.rs b/kolomoni_auth/src/lib.rs index be8a353..7990214 100644 --- a/kolomoni_auth/src/lib.rs +++ b/kolomoni_auth/src/lib.rs @@ -1,7 +1,9 @@ +mod hasher; mod permissions; mod roles; mod token; +pub use hasher::*; pub use permissions::*; pub use roles::*; pub use token::*; diff --git a/kolomoni_database/Cargo.toml b/kolomoni_database/Cargo.toml index 229ca97..42c6ccb 100644 --- a/kolomoni_database/Cargo.toml +++ b/kolomoni_database/Cargo.toml @@ -2,19 +2,19 @@ name = "kolomoni_database" version = "0.1.0" edition = "2021" -publish = false - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] kolomoni_auth = { path = "../kolomoni_auth" } -kolomoni_configuration = { path = "../kolomoni_configuration" } -thiserror = { workspace = true } -miette = { workspace = true } +futures-core = { workspace = true } +futures-util = { workspace = true } +pin-project-lite = { workspace = true } -# sea-orm = { workspace = true } +sqlx = { workspace = true } + +thiserror = { workspace = true } argon2 = { workspace = true } + chrono = { workspace = true } uuid = { workspace = true } diff --git a/kolomoni_database/src/entities/category.rs b/kolomoni_database/src/entities/category.rs deleted file mode 100644 index b8fa16e..0000000 --- a/kolomoni_database/src/entities/category.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.12 - -use sea_orm::entity::prelude::*; - -#[derive(Copy, Clone, Default, Debug, DeriveEntity)] -pub struct Entity; - -impl EntityName for Entity { - fn table_name(&self) -> &str { - "category" - } -} - -#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] -pub struct Model { - pub id: i32, - pub english_name: String, - pub slovene_name: String, - pub created_at: DateTimeWithTimeZone, - pub last_modified_at: DateTimeWithTimeZone, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] -pub enum Column { - Id, - EnglishName, - SloveneName, - CreatedAt, - LastModifiedAt, -} - -#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] -pub enum PrimaryKey { - Id, -} - -impl PrimaryKeyTrait for PrimaryKey { - type ValueType = i32; - fn auto_increment() -> bool { - true - } -} - -#[derive(Copy, Clone, Debug, EnumIter)] -pub enum Relation { - WordCategory, -} - -impl ColumnTrait for Column { - type EntityName = Entity; - fn def(&self) -> ColumnDef { - match self { - Self::Id => ColumnType::Integer.def(), - Self::EnglishName => ColumnType::String(None).def(), - Self::SloveneName => ColumnType::String(None).def(), - Self::CreatedAt => ColumnType::TimestampWithTimeZone.def(), - Self::LastModifiedAt => ColumnType::TimestampWithTimeZone.def(), - } - } -} - -impl RelationTrait for Relation { - fn def(&self) -> RelationDef { - match self { - Self::WordCategory => Entity::has_many(super::word_category::Entity).into(), - } - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::WordCategory.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - super::word_category::Relation::Word.def() - } - fn via() -> Option { - Some(super::word_category::Relation::Category.def().rev()) - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/kolomoni_database/src/entities/mod.rs b/kolomoni_database/src/entities/mod.rs index 3c001fd..595885b 100644 --- a/kolomoni_database/src/entities/mod.rs +++ b/kolomoni_database/src/entities/mod.rs @@ -1,16 +1,3 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.12 - -pub mod prelude; - -pub mod category; pub mod permission; pub mod role; -pub mod role_permission; pub mod user; -pub mod user_role; -pub mod word; -pub mod word_category; -pub mod word_english; -pub mod word_slovene; -pub mod word_translation; -pub mod word_translation_suggestion; diff --git a/kolomoni_database/src/entities/permission.rs b/kolomoni_database/src/entities/permission.rs deleted file mode 100644 index 37629a1..0000000 --- a/kolomoni_database/src/entities/permission.rs +++ /dev/null @@ -1,79 +0,0 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.12 - -use sea_orm::entity::prelude::*; - -#[derive(Copy, Clone, Default, Debug, DeriveEntity)] -pub struct Entity; - -impl EntityName for Entity { - fn table_name(&self) -> &str { - "permission" - } -} - -#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] -pub struct Model { - pub id: i32, - pub name: String, - pub description: String, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] -pub enum Column { - Id, - Name, - Description, -} - -#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] -pub enum PrimaryKey { - Id, -} - -impl PrimaryKeyTrait for PrimaryKey { - type ValueType = i32; - fn auto_increment() -> bool { - true - } -} - -#[derive(Copy, Clone, Debug, EnumIter)] -pub enum Relation { - RolePermission, -} - -impl ColumnTrait for Column { - type EntityName = Entity; - fn def(&self) -> ColumnDef { - match self { - Self::Id => ColumnType::Integer.def(), - Self::Name => ColumnType::String(None).def().unique(), - Self::Description => ColumnType::String(None).def(), - } - } -} - -impl RelationTrait for Relation { - fn def(&self) -> RelationDef { - match self { - Self::RolePermission => Entity::has_many(super::role_permission::Entity).into(), - } - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::RolePermission.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - super::role_permission::Relation::Role.def() - } - fn via() -> Option { - Some(super::role_permission::Relation::Permission.def().rev()) - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/kolomoni_database/src/entities/permission/mod.rs b/kolomoni_database/src/entities/permission/mod.rs new file mode 100644 index 0000000..1826ff6 --- /dev/null +++ b/kolomoni_database/src/entities/permission/mod.rs @@ -0,0 +1,7 @@ +mod model; +mod mutation; +mod query; + +pub use model::*; +pub use mutation::*; +pub use query::*; diff --git a/kolomoni_database/src/entities/permission/model.rs b/kolomoni_database/src/entities/permission/model.rs new file mode 100644 index 0000000..4e38db9 --- /dev/null +++ b/kolomoni_database/src/entities/permission/model.rs @@ -0,0 +1,19 @@ +pub struct FullModel { + /// Internal ID of the permission, don't expose externally. + pub id: i32, + + pub key: String, + + pub description_en: String, + + pub description_sl: String, +} + +pub struct ReducedModel { + /// Internal ID of the permission, don't expose externally. + pub id: i32, + + pub key: String, +} + +// TODO continue from here diff --git a/kolomoni_database/src/entities/permission/mutation.rs b/kolomoni_database/src/entities/permission/mutation.rs new file mode 100644 index 0000000..e69de29 diff --git a/kolomoni_database/src/entities/permission/query.rs b/kolomoni_database/src/entities/permission/query.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/kolomoni_database/src/entities/permission/query.rs @@ -0,0 +1 @@ + diff --git a/kolomoni_database/src/entities/prelude.rs b/kolomoni_database/src/entities/prelude.rs deleted file mode 100644 index c37c3d0..0000000 --- a/kolomoni_database/src/entities/prelude.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.12 - -pub use super::category::Entity as Category; -pub use super::permission::Entity as Permission; -pub use super::role::Entity as Role; -pub use super::role_permission::Entity as RolePermission; -pub use super::user::Entity as User; -pub use super::user_role::Entity as UserRole; -pub use super::word::Entity as Word; -pub use super::word_category::Entity as WordCategory; -pub use super::word_english::Entity as WordEnglish; -pub use super::word_slovene::Entity as WordSlovene; -pub use super::word_translation::Entity as WordTranslation; -pub use super::word_translation_suggestion::Entity as WordTranslationSuggestion; diff --git a/kolomoni_database/src/entities/role.rs b/kolomoni_database/src/entities/role.rs deleted file mode 100644 index d5f2f36..0000000 --- a/kolomoni_database/src/entities/role.rs +++ /dev/null @@ -1,96 +0,0 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.12 - -use sea_orm::entity::prelude::*; - -#[derive(Copy, Clone, Default, Debug, DeriveEntity)] -pub struct Entity; - -impl EntityName for Entity { - fn table_name(&self) -> &str { - "role" - } -} - -#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] -pub struct Model { - pub id: i32, - pub name: String, - pub description: String, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] -pub enum Column { - Id, - Name, - Description, -} - -#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] -pub enum PrimaryKey { - Id, -} - -impl PrimaryKeyTrait for PrimaryKey { - type ValueType = i32; - fn auto_increment() -> bool { - true - } -} - -#[derive(Copy, Clone, Debug, EnumIter)] -pub enum Relation { - RolePermission, - UserRole, -} - -impl ColumnTrait for Column { - type EntityName = Entity; - fn def(&self) -> ColumnDef { - match self { - Self::Id => ColumnType::Integer.def(), - Self::Name => ColumnType::String(None).def(), - Self::Description => ColumnType::String(None).def(), - } - } -} - -impl RelationTrait for Relation { - fn def(&self) -> RelationDef { - match self { - Self::RolePermission => Entity::has_many(super::role_permission::Entity).into(), - Self::UserRole => Entity::has_many(super::user_role::Entity).into(), - } - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::RolePermission.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::UserRole.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - super::role_permission::Relation::Permission.def() - } - fn via() -> Option { - Some(super::role_permission::Relation::Role.def().rev()) - } -} - -impl Related for Entity { - fn to() -> RelationDef { - super::user_role::Relation::User.def() - } - fn via() -> Option { - Some(super::user_role::Relation::Role.def().rev()) - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/kolomoni_database/src/entities/role/mod.rs b/kolomoni_database/src/entities/role/mod.rs new file mode 100644 index 0000000..8177ff8 --- /dev/null +++ b/kolomoni_database/src/entities/role/mod.rs @@ -0,0 +1,2 @@ +mod model; +pub use model::*; diff --git a/kolomoni_database/src/entities/role/model.rs b/kolomoni_database/src/entities/role/model.rs new file mode 100644 index 0000000..a6823fe --- /dev/null +++ b/kolomoni_database/src/entities/role/model.rs @@ -0,0 +1,15 @@ +pub struct FullModel { + pub id: i32, + + pub key: String, + + pub description_en: String, + + pub description_sl: String, +} + +pub struct ReducedModel { + pub id: i32, + + pub key: String, +} diff --git a/kolomoni_database/src/entities/role_permission.rs b/kolomoni_database/src/entities/role_permission.rs deleted file mode 100644 index a590441..0000000 --- a/kolomoni_database/src/entities/role_permission.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.12 - -use sea_orm::entity::prelude::*; - -#[derive(Copy, Clone, Default, Debug, DeriveEntity)] -pub struct Entity; - -impl EntityName for Entity { - fn table_name(&self) -> &str { - "role_permission" - } -} - -#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] -pub struct Model { - pub role_id: i32, - pub permission_id: i32, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] -pub enum Column { - RoleId, - PermissionId, -} - -#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] -pub enum PrimaryKey { - PermissionId, - RoleId, -} - -impl PrimaryKeyTrait for PrimaryKey { - type ValueType = (i32, i32); - fn auto_increment() -> bool { - false - } -} - -#[derive(Copy, Clone, Debug, EnumIter)] -pub enum Relation { - Permission, - Role, -} - -impl ColumnTrait for Column { - type EntityName = Entity; - fn def(&self) -> ColumnDef { - match self { - Self::RoleId => ColumnType::Integer.def(), - Self::PermissionId => ColumnType::Integer.def(), - } - } -} - -impl RelationTrait for Relation { - fn def(&self) -> RelationDef { - match self { - Self::Permission => Entity::belongs_to(super::permission::Entity) - .from(Column::PermissionId) - .to(super::permission::Column::Id) - .into(), - Self::Role => Entity::belongs_to(super::role::Entity) - .from(Column::RoleId) - .to(super::role::Column::Id) - .into(), - } - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Permission.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Role.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/kolomoni_database/src/entities/user.rs b/kolomoni_database/src/entities/user.rs deleted file mode 100644 index c6ccfaa..0000000 --- a/kolomoni_database/src/entities/user.rs +++ /dev/null @@ -1,91 +0,0 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.12 - -use sea_orm::entity::prelude::*; - -#[derive(Copy, Clone, Default, Debug, DeriveEntity)] -pub struct Entity; - -impl EntityName for Entity { - fn table_name(&self) -> &str { - "user" - } -} - -#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] -pub struct Model { - pub id: i32, - pub username: String, - pub display_name: String, - pub hashed_password: String, - pub joined_at: DateTimeWithTimeZone, - pub last_modified_at: DateTimeWithTimeZone, - pub last_active_at: DateTimeWithTimeZone, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] -pub enum Column { - Id, - Username, - DisplayName, - HashedPassword, - JoinedAt, - LastModifiedAt, - LastActiveAt, -} - -#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] -pub enum PrimaryKey { - Id, -} - -impl PrimaryKeyTrait for PrimaryKey { - type ValueType = i32; - fn auto_increment() -> bool { - true - } -} - -#[derive(Copy, Clone, Debug, EnumIter)] -pub enum Relation { - UserRole, -} - -impl ColumnTrait for Column { - type EntityName = Entity; - fn def(&self) -> ColumnDef { - match self { - Self::Id => ColumnType::Integer.def(), - Self::Username => ColumnType::String(None).def(), - Self::DisplayName => ColumnType::String(None).def(), - Self::HashedPassword => ColumnType::String(None).def(), - Self::JoinedAt => ColumnType::TimestampWithTimeZone.def(), - Self::LastModifiedAt => ColumnType::TimestampWithTimeZone.def(), - Self::LastActiveAt => ColumnType::TimestampWithTimeZone.def(), - } - } -} - -impl RelationTrait for Relation { - fn def(&self) -> RelationDef { - match self { - Self::UserRole => Entity::has_many(super::user_role::Entity).into(), - } - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::UserRole.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - super::user_role::Relation::Role.def() - } - fn via() -> Option { - Some(super::user_role::Relation::User.def().rev()) - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/kolomoni_database/src/entities/user/mod.rs b/kolomoni_database/src/entities/user/mod.rs new file mode 100644 index 0000000..d54a76e --- /dev/null +++ b/kolomoni_database/src/entities/user/mod.rs @@ -0,0 +1,4 @@ +mod model; +pub use model::*; +mod query; +pub use query::*; diff --git a/kolomoni_database/src/entities/user/model.rs b/kolomoni_database/src/entities/user/model.rs new file mode 100644 index 0000000..0c7d05e --- /dev/null +++ b/kolomoni_database/src/entities/user/model.rs @@ -0,0 +1,73 @@ +use chrono::{DateTime, Utc}; +use uuid::Uuid; + +use crate::IntoModel; + +#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct UserId(Uuid); + +impl UserId { + #[inline] + pub fn new(uuid: Uuid) -> Self { + Self(uuid) + } + + #[inline] + pub fn into_inner(self) -> Uuid { + self.0 + } +} + + +pub struct Model { + /// UUIDv7 + pub id: UserId, + + pub username: String, + + pub display_name: String, + + pub hashed_password: String, + + pub joined_at: DateTime, + + pub last_modified_at: DateTime, + + pub last_active_at: DateTime, +} + + +pub(super) struct IntermediateModel { + /// UUIDv7 + pub id: Uuid, + + pub username: String, + + pub display_name: String, + + pub hashed_password: String, + + pub joined_at: DateTime, + + pub last_modified_at: DateTime, + + pub last_active_at: DateTime, +} + +impl IntoModel for IntermediateModel { + type Model = Model; + + fn into_model(self) -> Self::Model { + let user_id = UserId::new(self.id); + + Self::Model { + id: user_id, + username: self.username, + display_name: self.display_name, + hashed_password: self.hashed_password, + joined_at: self.joined_at, + last_modified_at: self.last_modified_at, + last_active_at: self.last_active_at, + } + } +} diff --git a/kolomoni_database/src/entities/user/query.rs b/kolomoni_database/src/entities/user/query.rs new file mode 100644 index 0000000..1cc57dd --- /dev/null +++ b/kolomoni_database/src/entities/user/query.rs @@ -0,0 +1,211 @@ +use std::{borrow::Cow, pin::Pin}; + +use futures_core::{stream::BoxStream, Stream}; +use kolomoni_auth::{ArgonHasher, ArgonHasherError}; +use pin_project_lite::pin_project; +use sqlx::PgConnection; +use thiserror::Error; + +use super::UserId; +use crate::{IntoModel, QueryError, QueryResult}; + + +#[derive(Debug, Error)] +pub enum UserCredentialValidationError { + #[error("sqlx error")] + SqlxError { + #[source] + error: sqlx::Error, + }, + + #[error("model error: {}", .reason)] + ModelError { reason: Cow<'static, str> }, + + #[error("hasher error")] + HasherError { + #[source] + error: ArgonHasherError, + }, +} + +impl From for UserCredentialValidationError { + fn from(value: QueryError) -> Self { + match value { + QueryError::SqlxError { error } => Self::SqlxError { error }, + QueryError::ModelError { reason } => Self::ModelError { reason }, + } + } +} + + + +type RawUserListStream<'c> = BoxStream<'c, Result>; + + +pin_project! { + pub struct UserListStream<'c> { + #[pin] + inner: RawUserListStream<'c>, + } +} + +impl<'c> UserListStream<'c> { + #[inline] + fn from_raw_stream(stream: RawUserListStream<'c>) -> Self { + Self { inner: stream } + } +} + +impl<'c> Stream for UserListStream<'c> { + type Item = QueryResult; + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let this = self.project(); + + match this.inner.poll_next(cx) { + std::task::Poll::Ready(ready) => std::task::Poll::Ready(ready.map(|result| { + result + .map(super::IntermediateModel::into_model) + .map_err(|error| QueryError::SqlxError { error }) + })), + std::task::Poll::Pending => std::task::Poll::Pending, + } + } +} + + + + +pub struct Query; + +impl Query { + pub async fn get_user_by_id( + connection: &mut PgConnection, + user_id: UserId, + ) -> QueryResult> { + let optional_intermediate_model = sqlx::query_as!( + super::IntermediateModel, + "SELECT \ + id, username, display_name, hashed_password, \ + joined_at, last_modified_at, last_active_at \ + FROM kolomoni.user \ + WHERE id = $1", + user_id.into_inner() + ) + .fetch_optional(connection) + .await?; + + Ok(optional_intermediate_model.map(super::IntermediateModel::into_model)) + } + + pub async fn get_user_by_username( + connection: &mut PgConnection, + username: U, + ) -> QueryResult> + where + U: AsRef, + { + let optional_intermediate_model = sqlx::query_as!( + super::IntermediateModel, + "SELECT \ + id, username, display_name, hashed_password, \ + joined_at, last_modified_at, last_active_at \ + FROM kolomoni.user \ + WHERE username = $1", + username.as_ref() + ) + .fetch_optional(connection) + .await?; + + Ok(optional_intermediate_model.map(super::IntermediateModel::into_model)) + } + + pub async fn exists_by_id(connection: &mut PgConnection, user_id: UserId) -> QueryResult { + sqlx::query_scalar!( + "SELECT EXISTS (SELECT 1 FROM kolomoni.user WHERE id = $1)", + user_id.into_inner() + ) + .fetch_one(connection) + .await + .map(|exists| exists.unwrap_or(false)) + .map_err(|error| QueryError::SqlxError { error }) + } + + pub async fn exists_by_username( + connection: &mut PgConnection, + username: U, + ) -> QueryResult + where + U: AsRef, + { + sqlx::query_scalar!( + "SELECT EXISTS (SELECT 1 FROM kolomoni.user WHERE username = $1)", + username.as_ref() + ) + .fetch_one(connection) + .await + .map(|exists| exists.unwrap_or(false)) + .map_err(|error| QueryError::SqlxError { error }) + } + + pub async fn exists_by_display_name( + connection: &mut PgConnection, + display_name: U, + ) -> QueryResult + where + U: AsRef, + { + sqlx::query_scalar!( + "SELECT EXISTS (SELECT 1 FROM kolomoni.user WHERE display_name = $1)", + display_name.as_ref() + ) + .fetch_one(connection) + .await + .map(|exists| exists.unwrap_or(false)) + .map_err(|error| QueryError::SqlxError { error }) + } + + pub async fn validate_credentials( + connection: &mut PgConnection, + hasher: &ArgonHasher, + username: U, + password: P, + ) -> QueryResult, UserCredentialValidationError> + where + U: AsRef, + P: AsRef, + { + let potential_user = Self::get_user_by_username(connection, username).await?; + + let Some(user) = potential_user else { + return Ok(None); + }; + + let is_valid_password = hasher + .verify_password_against_hash(password.as_ref(), &user.hashed_password) + .map_err(|error| UserCredentialValidationError::HasherError { error })?; + + + if is_valid_password { + Ok(Some(user)) + } else { + Ok(None) + } + } + + pub fn get_all_users(connection: &mut PgConnection) -> UserListStream<'_> { + let user_stream = sqlx::query_as!( + super::IntermediateModel, + "SELECT \ + id, username, display_name, hashed_password, \ + joined_at, last_modified_at, last_active_at \ + FROM kolomoni.user" + ) + .fetch(connection); + + UserListStream::from_raw_stream(user_stream) + } +} diff --git a/kolomoni_database/src/entities/user_role.rs b/kolomoni_database/src/entities/user_role.rs deleted file mode 100644 index 58a29f8..0000000 --- a/kolomoni_database/src/entities/user_role.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.12 - -use sea_orm::entity::prelude::*; - -#[derive(Copy, Clone, Default, Debug, DeriveEntity)] -pub struct Entity; - -impl EntityName for Entity { - fn table_name(&self) -> &str { - "user_role" - } -} - -#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] -pub struct Model { - pub user_id: i32, - pub role_id: i32, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] -pub enum Column { - UserId, - RoleId, -} - -#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] -pub enum PrimaryKey { - UserId, - RoleId, -} - -impl PrimaryKeyTrait for PrimaryKey { - type ValueType = (i32, i32); - fn auto_increment() -> bool { - false - } -} - -#[derive(Copy, Clone, Debug, EnumIter)] -pub enum Relation { - Role, - User, -} - -impl ColumnTrait for Column { - type EntityName = Entity; - fn def(&self) -> ColumnDef { - match self { - Self::UserId => ColumnType::Integer.def(), - Self::RoleId => ColumnType::Integer.def(), - } - } -} - -impl RelationTrait for Relation { - fn def(&self) -> RelationDef { - match self { - Self::Role => Entity::belongs_to(super::role::Entity) - .from(Column::RoleId) - .to(super::role::Column::Id) - .into(), - Self::User => Entity::belongs_to(super::user::Entity) - .from(Column::UserId) - .to(super::user::Column::Id) - .into(), - } - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Role.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::User.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/kolomoni_database/src/entities/word.rs b/kolomoni_database/src/entities/word.rs deleted file mode 100644 index 6ee21ba..0000000 --- a/kolomoni_database/src/entities/word.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.12 - -use sea_orm::entity::prelude::*; - -#[derive(Copy, Clone, Default, Debug, DeriveEntity)] -pub struct Entity; - -impl EntityName for Entity { - fn table_name(&self) -> &str { - "word" - } -} - -#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] -pub struct Model { - pub id: Uuid, - pub language: String, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] -pub enum Column { - Id, - Language, -} - -#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] -pub enum PrimaryKey { - Id, -} - -impl PrimaryKeyTrait for PrimaryKey { - type ValueType = Uuid; - fn auto_increment() -> bool { - false - } -} - -#[derive(Copy, Clone, Debug, EnumIter)] -pub enum Relation { - WordCategory, - WordEnglish, - WordSlovene, -} - -impl ColumnTrait for Column { - type EntityName = Entity; - fn def(&self) -> ColumnDef { - match self { - Self::Id => ColumnType::Uuid.def(), - Self::Language => ColumnType::String(Some(12u32)).def(), - } - } -} - -impl RelationTrait for Relation { - fn def(&self) -> RelationDef { - match self { - Self::WordCategory => Entity::has_many(super::word_category::Entity).into(), - Self::WordEnglish => Entity::has_many(super::word_english::Entity).into(), - Self::WordSlovene => Entity::has_many(super::word_slovene::Entity).into(), - } - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::WordCategory.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::WordEnglish.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::WordSlovene.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - super::word_category::Relation::Category.def() - } - fn via() -> Option { - Some(super::word_category::Relation::Word.def().rev()) - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/kolomoni_database/src/entities/word_category.rs b/kolomoni_database/src/entities/word_category.rs deleted file mode 100644 index eba832a..0000000 --- a/kolomoni_database/src/entities/word_category.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.12 - -use sea_orm::entity::prelude::*; - -#[derive(Copy, Clone, Default, Debug, DeriveEntity)] -pub struct Entity; - -impl EntityName for Entity { - fn table_name(&self) -> &str { - "word_category" - } -} - -#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] -pub struct Model { - pub word_id: Uuid, - pub category_id: i32, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] -pub enum Column { - WordId, - CategoryId, -} - -#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] -pub enum PrimaryKey { - WordId, - CategoryId, -} - -impl PrimaryKeyTrait for PrimaryKey { - type ValueType = (Uuid, i32); - fn auto_increment() -> bool { - false - } -} - -#[derive(Copy, Clone, Debug, EnumIter)] -pub enum Relation { - Category, - Word, -} - -impl ColumnTrait for Column { - type EntityName = Entity; - fn def(&self) -> ColumnDef { - match self { - Self::WordId => ColumnType::Uuid.def(), - Self::CategoryId => ColumnType::Integer.def(), - } - } -} - -impl RelationTrait for Relation { - fn def(&self) -> RelationDef { - match self { - Self::Category => Entity::belongs_to(super::category::Entity) - .from(Column::CategoryId) - .to(super::category::Column::Id) - .into(), - Self::Word => Entity::belongs_to(super::word::Entity) - .from(Column::WordId) - .to(super::word::Column::Id) - .into(), - } - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Category.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Word.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/kolomoni_database/src/entities/word_english.rs b/kolomoni_database/src/entities/word_english.rs deleted file mode 100644 index e00aa73..0000000 --- a/kolomoni_database/src/entities/word_english.rs +++ /dev/null @@ -1,100 +0,0 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.12 - -use sea_orm::entity::prelude::*; - -#[derive(Copy, Clone, Default, Debug, DeriveEntity)] -pub struct Entity; - -impl EntityName for Entity { - fn table_name(&self) -> &str { - "word_english" - } -} - -#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] -pub struct Model { - pub word_id: Uuid, - pub lemma: String, - pub disambiguation: Option, - pub description: Option, - pub created_at: DateTimeWithTimeZone, - pub last_modified_at: DateTimeWithTimeZone, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] -pub enum Column { - WordId, - Lemma, - Disambiguation, - Description, - CreatedAt, - LastModifiedAt, -} - -#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] -pub enum PrimaryKey { - WordId, -} - -impl PrimaryKeyTrait for PrimaryKey { - type ValueType = Uuid; - fn auto_increment() -> bool { - false - } -} - -#[derive(Copy, Clone, Debug, EnumIter)] -pub enum Relation { - Word, - WordTranslation, - WordTranslationSuggestion, -} - -impl ColumnTrait for Column { - type EntityName = Entity; - fn def(&self) -> ColumnDef { - match self { - Self::WordId => ColumnType::Uuid.def(), - Self::Lemma => ColumnType::String(None).def(), - Self::Disambiguation => ColumnType::String(None).def().null(), - Self::Description => ColumnType::String(None).def().null(), - Self::CreatedAt => ColumnType::TimestampWithTimeZone.def(), - Self::LastModifiedAt => ColumnType::TimestampWithTimeZone.def(), - } - } -} - -impl RelationTrait for Relation { - fn def(&self) -> RelationDef { - match self { - Self::Word => Entity::belongs_to(super::word::Entity) - .from(Column::WordId) - .to(super::word::Column::Id) - .into(), - Self::WordTranslation => Entity::has_many(super::word_translation::Entity).into(), - Self::WordTranslationSuggestion => { - Entity::has_many(super::word_translation_suggestion::Entity).into() - } - } - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Word.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::WordTranslation.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::WordTranslationSuggestion.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/kolomoni_database/src/entities/word_slovene.rs b/kolomoni_database/src/entities/word_slovene.rs deleted file mode 100644 index 0bda73c..0000000 --- a/kolomoni_database/src/entities/word_slovene.rs +++ /dev/null @@ -1,100 +0,0 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.12 - -use sea_orm::entity::prelude::*; - -#[derive(Copy, Clone, Default, Debug, DeriveEntity)] -pub struct Entity; - -impl EntityName for Entity { - fn table_name(&self) -> &str { - "word_slovene" - } -} - -#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] -pub struct Model { - pub word_id: Uuid, - pub lemma: String, - pub disambiguation: Option, - pub description: Option, - pub created_at: DateTimeWithTimeZone, - pub last_modified_at: DateTimeWithTimeZone, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] -pub enum Column { - WordId, - Lemma, - Disambiguation, - Description, - CreatedAt, - LastModifiedAt, -} - -#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] -pub enum PrimaryKey { - WordId, -} - -impl PrimaryKeyTrait for PrimaryKey { - type ValueType = Uuid; - fn auto_increment() -> bool { - false - } -} - -#[derive(Copy, Clone, Debug, EnumIter)] -pub enum Relation { - Word, - WordTranslation, - WordTranslationSuggestion, -} - -impl ColumnTrait for Column { - type EntityName = Entity; - fn def(&self) -> ColumnDef { - match self { - Self::WordId => ColumnType::Uuid.def(), - Self::Lemma => ColumnType::String(None).def(), - Self::Disambiguation => ColumnType::String(None).def().null(), - Self::Description => ColumnType::String(None).def().null(), - Self::CreatedAt => ColumnType::TimestampWithTimeZone.def(), - Self::LastModifiedAt => ColumnType::TimestampWithTimeZone.def(), - } - } -} - -impl RelationTrait for Relation { - fn def(&self) -> RelationDef { - match self { - Self::Word => Entity::belongs_to(super::word::Entity) - .from(Column::WordId) - .to(super::word::Column::Id) - .into(), - Self::WordTranslation => Entity::has_many(super::word_translation::Entity).into(), - Self::WordTranslationSuggestion => { - Entity::has_many(super::word_translation_suggestion::Entity).into() - } - } - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Word.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::WordTranslation.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::WordTranslationSuggestion.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/kolomoni_database/src/entities/word_translation.rs b/kolomoni_database/src/entities/word_translation.rs deleted file mode 100644 index 914b668..0000000 --- a/kolomoni_database/src/entities/word_translation.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.12 - -use sea_orm::entity::prelude::*; - -#[derive(Copy, Clone, Default, Debug, DeriveEntity)] -pub struct Entity; - -impl EntityName for Entity { - fn table_name(&self) -> &str { - "word_translation" - } -} - -#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] -pub struct Model { - pub english_word_id: Uuid, - pub slovene_word_id: Uuid, - pub translated_at: DateTimeWithTimeZone, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] -pub enum Column { - EnglishWordId, - SloveneWordId, - TranslatedAt, -} - -#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] -pub enum PrimaryKey { - EnglishWordId, - SloveneWordId, -} - -impl PrimaryKeyTrait for PrimaryKey { - type ValueType = (Uuid, Uuid); - fn auto_increment() -> bool { - false - } -} - -#[derive(Copy, Clone, Debug, EnumIter)] -pub enum Relation { - WordEnglish, - WordSlovene, -} - -impl ColumnTrait for Column { - type EntityName = Entity; - fn def(&self) -> ColumnDef { - match self { - Self::EnglishWordId => ColumnType::Uuid.def(), - Self::SloveneWordId => ColumnType::Uuid.def(), - Self::TranslatedAt => ColumnType::TimestampWithTimeZone.def(), - } - } -} - -impl RelationTrait for Relation { - fn def(&self) -> RelationDef { - match self { - Self::WordEnglish => Entity::belongs_to(super::word_english::Entity) - .from(Column::EnglishWordId) - .to(super::word_english::Column::WordId) - .into(), - Self::WordSlovene => Entity::belongs_to(super::word_slovene::Entity) - .from(Column::SloveneWordId) - .to(super::word_slovene::Column::WordId) - .into(), - } - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::WordEnglish.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::WordSlovene.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/kolomoni_database/src/entities/word_translation_suggestion.rs b/kolomoni_database/src/entities/word_translation_suggestion.rs deleted file mode 100644 index 70605a1..0000000 --- a/kolomoni_database/src/entities/word_translation_suggestion.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.12 - -use sea_orm::entity::prelude::*; - -#[derive(Copy, Clone, Default, Debug, DeriveEntity)] -pub struct Entity; - -impl EntityName for Entity { - fn table_name(&self) -> &str { - "word_translation_suggestion" - } -} - -#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Eq)] -pub struct Model { - pub english_word_id: Uuid, - pub slovene_word_id: Uuid, - pub suggested_at: DateTimeWithTimeZone, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] -pub enum Column { - EnglishWordId, - SloveneWordId, - SuggestedAt, -} - -#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] -pub enum PrimaryKey { - EnglishWordId, - SloveneWordId, -} - -impl PrimaryKeyTrait for PrimaryKey { - type ValueType = (Uuid, Uuid); - fn auto_increment() -> bool { - false - } -} - -#[derive(Copy, Clone, Debug, EnumIter)] -pub enum Relation { - WordEnglish, - WordSlovene, -} - -impl ColumnTrait for Column { - type EntityName = Entity; - fn def(&self) -> ColumnDef { - match self { - Self::EnglishWordId => ColumnType::Uuid.def(), - Self::SloveneWordId => ColumnType::Uuid.def(), - Self::SuggestedAt => ColumnType::TimestampWithTimeZone.def(), - } - } -} - -impl RelationTrait for Relation { - fn def(&self) -> RelationDef { - match self { - Self::WordEnglish => Entity::belongs_to(super::word_english::Entity) - .from(Column::EnglishWordId) - .to(super::word_english::Column::WordId) - .into(), - Self::WordSlovene => Entity::belongs_to(super::word_slovene::Entity) - .from(Column::SloveneWordId) - .to(super::word_slovene::Column::WordId) - .into(), - } - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::WordEnglish.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::WordSlovene.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/kolomoni_database/src/impls.rs b/kolomoni_database/src/impls.rs deleted file mode 100644 index abccad3..0000000 --- a/kolomoni_database/src/impls.rs +++ /dev/null @@ -1 +0,0 @@ -mod word; diff --git a/kolomoni_database/src/impls/word.rs b/kolomoni_database/src/impls/word.rs deleted file mode 100644 index 454f3a3..0000000 --- a/kolomoni_database/src/impls/word.rs +++ /dev/null @@ -1,16 +0,0 @@ -use miette::{miette, Context, IntoDiagnostic, Result}; - -use crate::{entities, shared::WordLanguage}; - -impl entities::word::Model { - pub fn language(&self) -> Result { - WordLanguage::from_ietf_language_tag(&self.language) - .into_diagnostic() - .wrap_err_with(|| { - miette!( - "Failed to convert IETF language tag to WordLanguage: {}", - self.language - ) - }) - } -} diff --git a/kolomoni_database/src/lib.rs b/kolomoni_database/src/lib.rs index 8d6cb29..120d091 100644 --- a/kolomoni_database/src/lib.rs +++ b/kolomoni_database/src/lib.rs @@ -1,19 +1,35 @@ -#![allow(rustdoc::private_intra_doc_links)] - -//! This crate contains raw database entities in combination -//! with the "business logic", i.e. query and mutation methods for them. -//! -//! *Do not query or mutate the models by hand! Use the querying and mutation -//! implementations in the [`query`] and [`mutation`] modules instead!* -//! -//! The entitites in [`entities`] must be auto-generated by `sea-orm-cli`; -//! for more information, consult the README. -//! -//! Additional `impl` blocks for the auto-generated entities are present in the [`impls`] module. +use std::borrow::Cow; + +use thiserror::Error; pub mod entities; -mod impls; -pub mod macros; -pub mod mutation; -pub mod query; -pub mod shared; + + +#[derive(Debug, Error)] +pub enum QueryError { + #[error("sqlx error")] + SqlxError { + #[from] + #[source] + error: sqlx::Error, + }, + + #[error("model error: {}", .reason)] + ModelError { reason: Cow<'static, str> }, +} + +pub type QueryResult = Result; + + +pub(crate) trait IntoModel { + type Model; + + fn into_model(self) -> Self::Model; +} + +pub(crate) trait TryIntoModel { + type Model; + type Error; + + fn try_into_model(self) -> Result; +} diff --git a/kolomoni_database/src/macros.rs b/kolomoni_database/src/macros.rs deleted file mode 100644 index 190ee3d..0000000 --- a/kolomoni_database/src/macros.rs +++ /dev/null @@ -1,26 +0,0 @@ -#[macro_export] -macro_rules! begin_transaction { - ($database:expr) => {{ - let _transaction_result = sea_orm::TransactionTrait::begin($database).await; - let _transaction_diagnostic = miette::IntoDiagnostic::into_diagnostic(_transaction_result); - - miette::Context::wrap_err( - _transaction_diagnostic, - "Failed to begin database transaction.", - ) - }}; -} - - -#[macro_export] -macro_rules! commit_transaction { - ($transaction:expr) => {{ - let _commit_result = $transaction.commit().await; - let _commit_diagnostic = miette::IntoDiagnostic::into_diagnostic(_commit_result); - - miette::Context::wrap_err( - _commit_diagnostic, - "Failed to commit database transaction.", - ) - }}; -} diff --git a/kolomoni_database/src/mutation.rs b/kolomoni_database/src/mutation.rs deleted file mode 100644 index 2e43473..0000000 --- a/kolomoni_database/src/mutation.rs +++ /dev/null @@ -1,19 +0,0 @@ -mod category; -mod user; -mod user_role; -mod word; -mod word_category; -mod word_english; -mod word_slovene; -mod word_translation; -mod word_translation_suggestion; - -pub use category::*; -pub use user::*; -pub use user_role::*; -pub use word::*; -pub use word_category::*; -pub use word_english::*; -pub use word_slovene::*; -pub use word_translation::*; -pub use word_translation_suggestion::*; diff --git a/kolomoni_database/src/mutation/category.rs b/kolomoni_database/src/mutation/category.rs deleted file mode 100644 index 08c6ab8..0000000 --- a/kolomoni_database/src/mutation/category.rs +++ /dev/null @@ -1,101 +0,0 @@ -use chrono::Utc; -use miette::{miette, Context, IntoDiagnostic, Result}; -use sea_orm::{ActiveModelTrait, ActiveValue, ConnectionTrait, TransactionTrait}; - -use crate::entities::category; - - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct NewCategory { - pub slovene_name: String, - pub english_name: String, -} - - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct UpdatedCategory { - pub slovene_name: Option, - pub english_name: Option, -} - - -pub struct CategoryMutation; - -impl CategoryMutation { - pub async fn create( - database: &C, - category: NewCategory, - ) -> Result { - let creation_time = Utc::now().fixed_offset(); - - let active_category = category::ActiveModel { - slovene_name: ActiveValue::Set(category.slovene_name), - english_name: ActiveValue::Set(category.english_name), - created_at: ActiveValue::Set(creation_time), - last_modified_at: ActiveValue::Set(creation_time), - ..Default::default() - }; - - let new_category = active_category - .insert(database) - .await - .into_diagnostic() - .wrap_err("Failed to insert category into the database.")?; - - Ok(new_category) - } - - pub async fn update( - database: &C, - category_id: i32, - update: UpdatedCategory, - ) -> Result { - let mut active_category = category::ActiveModel { - id: ActiveValue::Unchanged(category_id), - last_modified_at: ActiveValue::Set(Utc::now().fixed_offset()), - ..Default::default() - }; - - if let Some(updated_slovene_name) = update.slovene_name { - active_category.slovene_name = ActiveValue::Set(updated_slovene_name); - } - - if let Some(updated_english_name) = update.english_name { - active_category.english_name = ActiveValue::Set(updated_english_name); - } - - - let updated_category = active_category - .update(database) - .await - .into_diagnostic() - .wrap_err("Failed while updating category in database.")?; - - Ok(updated_category) - } - - pub async fn delete( - database: &C, - category_id: i32, - ) -> Result<()> { - let active_category = category::ActiveModel { - id: ActiveValue::Unchanged(category_id), - ..Default::default() - }; - - let deletion_result = active_category - .delete(database) - .await - .into_diagnostic() - .wrap_err("Failed to delete category from the database.")?; - - - if deletion_result.rows_affected == 1 { - Ok(()) - } else { - Err(miette!( - "Failed to delete category from the database: no such database." - )) - } - } -} diff --git a/kolomoni_database/src/mutation/user.rs b/kolomoni_database/src/mutation/user.rs deleted file mode 100644 index 5440c54..0000000 --- a/kolomoni_database/src/mutation/user.rs +++ /dev/null @@ -1,241 +0,0 @@ -use argon2::password_hash::SaltString; -use argon2::{Algorithm, Argon2, Params, PasswordHash, PasswordHasher, PasswordVerifier, Version}; -use chrono::{DateTime, Utc}; -use kolomoni_auth::DEFAULT_USER_ROLE; -use kolomoni_configuration::Configuration; -use miette::{miette, Context, IntoDiagnostic, Result}; -use sea_orm::{ - ActiveModelTrait, - ActiveValue, - ColumnTrait, - ConnectionTrait, - EntityTrait, - QueryFilter, - TransactionTrait, -}; - -use super::super::entities::user; -use crate::entities::user_role; -use crate::{begin_transaction, commit_transaction, query}; - - -pub struct ArgonHasher { - salt_string: SaltString, - argon_hasher: Argon2<'static>, -} - -impl ArgonHasher { - pub fn new(config: &Configuration) -> Result { - let salt_string = SaltString::from_b64(&config.secrets.hash_salt) - .map_err(|error| miette!("Failed to initialize SaltString: {error}."))?; - - let argon_hasher = Argon2::new( - Algorithm::Argon2id, - Version::V0x13, - Params::default(), - ); - - Ok(Self { - salt_string, - argon_hasher, - }) - } - - pub fn hash_password(&self, password: &str) -> Result { - self.argon_hasher - .hash_password(password.as_bytes(), &self.salt_string) - .map_err(|error| miette!("Errored while hashing password: {error}")) - } - - pub fn verify_password_against_hash( - &self, - password: &str, - hashed_password: &str, - ) -> Result { - let hashed_password = PasswordHash::new(hashed_password) - .map_err(|error| miette!("Errored while parsing hashed password: {error}"))?; - - Ok(self - .argon_hasher - .verify_password(password.as_bytes(), &hashed_password) - .is_ok()) - } -} - - - -pub struct UserRegistrationInfo { - pub username: String, - pub display_name: String, - pub password: String, -} - - - -/// Mutations for the [`crate::entities::user::Entity`] entity. -pub struct UserMutation; - -impl UserMutation { - /// Create a new user. - pub async fn create_user( - database: &C, - hasher: &ArgonHasher, - registration_info: UserRegistrationInfo, - ) -> Result { - let transaction = begin_transaction!(database)?; - - - // Hash password and register user into database. - let hashed_password = hasher - .hash_password(®istration_info.password) - .wrap_err("Failed to hash password.")?; - - let registration_time = Utc::now(); - - let user = user::ActiveModel { - username: ActiveValue::Set(registration_info.username), - display_name: ActiveValue::Set(registration_info.display_name), - hashed_password: ActiveValue::Set(hashed_password.to_string()), - joined_at: ActiveValue::Set(registration_time.fixed_offset()), - last_modified_at: ActiveValue::Set(registration_time.fixed_offset()), - last_active_at: ActiveValue::Set(registration_time.fixed_offset()), - ..Default::default() - } - .insert(&transaction) - .await - .into_diagnostic() - .wrap_err("Failed to save user into database.")?; - - - user_role::ActiveModel { - role_id: ActiveValue::Set(DEFAULT_USER_ROLE.id()), - user_id: ActiveValue::Set(user.id), - } - .insert(&transaction) - .await - .into_diagnostic() - .wrap_err_with(|| { - miette!( - "Failed to add the default role ({}) to user.", - DEFAULT_USER_ROLE.name() - ) - })?; - - - commit_transaction!(transaction)?; - Ok(user) - } - - /// Update last activity time for a user. The user is looked up by their username. - pub async fn update_last_active_at_by_username( - database: &C, - username: &str, - last_active_at: Option>, - ) -> Result { - // TODO This can be further optimized by using a lower-level query. - - let user = query::UserQuery::get_user_by_username(database, username) - .await? - .ok_or_else(|| miette!("Invalid username, no such user."))?; - - let user_with_updated_last_activity = user::ActiveModel { - id: ActiveValue::Unchanged(user.id), - last_active_at: ActiveValue::Set(last_active_at.unwrap_or_else(Utc::now).fixed_offset()), - ..Default::default() - }; - - let updated_user = user_with_updated_last_activity - .update(database) - .await - .into_diagnostic() - .wrap_err("Failed while updating a user's last activity time (by username).")?; - - Ok(updated_user) - } - - /// Update last activity time for a user. The user is looked up by their ID. - pub async fn update_last_active_at_by_user_id( - database: &C, - user_id: i32, - last_active_at: Option>, - ) -> Result { - let user_with_updated_last_activity = user::ActiveModel { - id: ActiveValue::Unchanged(user_id), - last_active_at: ActiveValue::Set(last_active_at.unwrap_or_else(Utc::now).fixed_offset()), - ..Default::default() - }; - - let updated_user = user_with_updated_last_activity - .update(database) - .await - .into_diagnostic() - .wrap_err("Failed while updating a user's last activity time (by ID).")?; - - Ok(updated_user) - } - - /// Update a user's display name. The user is looked up by their ID. - pub async fn update_display_name_by_user_id( - database: &C, - user_id: i32, - new_display_name: String, - ) -> Result { - let user_with_updated_display_name = user::ActiveModel { - id: ActiveValue::Unchanged(user_id), - display_name: ActiveValue::Set(new_display_name), - last_modified_at: ActiveValue::Set(Utc::now().fixed_offset()), - ..Default::default() - }; - - user_with_updated_display_name - .save(database) - .await - .into_diagnostic() - .wrap_err("Failed while updating a user's display name in the database (by ID).")?; - - let updated_user = query::UserQuery::get_user_by_id(database, user_id) - .await? - .ok_or_else(|| miette!("BUG: No such user ID: {user_id}"))?; - - Ok(updated_user) - } - - /// Update a user's display name. The user is looked up by their username. - pub async fn update_display_name_by_username( - database: &C, - username: &str, - new_display_name: String, - ) -> Result { - let current_time = Utc::now().fixed_offset(); - - let user_with_updated_display_name = user::ActiveModel { - display_name: ActiveValue::Set(new_display_name), - last_active_at: ActiveValue::Set(current_time), - last_modified_at: ActiveValue::Set(current_time), - ..Default::default() - }; - - let result = user::Entity::update_many() - .set(user_with_updated_display_name) - .filter(user::Column::Username.eq(username)) - .exec(database) - .await - .into_diagnostic() - .wrap_err( - "Failed while updating a user's display name in the database (by username).", - )?; - - if result.rows_affected != 1 { - return Err(miette!( - "BUG: Updated {} rows instead of 1!", - result.rows_affected - )); - } - - let updated_user = query::UserQuery::get_user_by_username(database, username) - .await? - .ok_or_else(|| miette!("BUG: No such user: {username}"))?; - - Ok(updated_user) - } -} diff --git a/kolomoni_database/src/mutation/user_role.rs b/kolomoni_database/src/mutation/user_role.rs deleted file mode 100644 index 963cb88..0000000 --- a/kolomoni_database/src/mutation/user_role.rs +++ /dev/null @@ -1,89 +0,0 @@ -use kolomoni_auth::Role; -use miette::{Context, IntoDiagnostic, Result}; -use sea_orm::{ - sea_query::OnConflict, - ActiveValue, - ColumnTrait, - ConnectionTrait, - EntityTrait, - QueryFilter, -}; - -use crate::entities; - -pub struct UserRoleMutation; - -impl UserRoleMutation { - pub async fn add_roles_to_user( - database: &C, - user_id: i32, - roles: &[Role], - ) -> Result<()> { - if roles.is_empty() { - return Ok(()); - } - - let role_models = roles - .iter() - .map(|role| entities::user_role::ActiveModel { - user_id: ActiveValue::Set(user_id), - role_id: ActiveValue::Set(role.id()), - }) - .collect::>(); - - entities::user_role::Entity::insert_many(role_models) - .on_conflict(OnConflict::new().do_nothing().to_owned()) - .exec_without_returning(database) - .await - .into_diagnostic() - .wrap_err("Failed while adding roles to user.")?; - - Ok(()) - } - - pub async fn remove_roles_from_user( - database: &C, - user_id: i32, - roles: &[Role], - ) -> Result<()> { - if roles.is_empty() { - return Ok(()); - } - - // The following code generates a set of AND and OR expressions - // to the following effect: - // (user_id matches) AND ((first role_id matches) OR (second role_id matches) OR ...) - // This allows us to remove all the specified roles with one database interaction. - - let base_removal_condition = entities::user_role::Column::UserId.eq(user_id); - - let role_id_removal_conditions = roles - .iter() - .map(|role| entities::user_role::Column::RoleId.eq(role.id())) - .collect::>(); - let merged_role_id_conditions = { - let mut condition_iterator = role_id_removal_conditions.into_iter(); - - // PANIC SAFETY: We checked that `roles` wasn't empty. - let mut current_condition = condition_iterator.next().unwrap(); - - for next_condition in condition_iterator { - current_condition = current_condition.or(next_condition); - } - - current_condition - }; - - - let final_master_condition = base_removal_condition.and(merged_role_id_conditions); - - entities::user_role::Entity::delete_many() - .filter(final_master_condition) - .exec(database) - .await - .into_diagnostic() - .wrap_err("Failed while removing roles from user.")?; - - Ok(()) - } -} diff --git a/kolomoni_database/src/mutation/word.rs b/kolomoni_database/src/mutation/word.rs deleted file mode 100644 index 319a993..0000000 --- a/kolomoni_database/src/mutation/word.rs +++ /dev/null @@ -1,32 +0,0 @@ -use miette::{miette, Context, IntoDiagnostic, Result}; -use sea_orm::{ActiveModelTrait, ActiveValue, ConnectionTrait, TransactionTrait}; -use uuid::Uuid; - -use crate::entities; - -pub struct WordMutation; - -impl WordMutation { - pub async fn delete( - database: &C, - word_uuid: Uuid, - ) -> Result<()> { - let active_word_model = entities::word::ActiveModel { - id: ActiveValue::Unchanged(word_uuid), - ..Default::default() - }; - - let deletion_result = active_word_model - .delete(database) - .await - .into_diagnostic() - .wrap_err("Failed while trying to delete a word from the database.")?; - - debug_assert!(deletion_result.rows_affected <= 1); - if deletion_result.rows_affected != 1 { - return Err(miette!("no word with the given UUID")); - } - - Ok(()) - } -} diff --git a/kolomoni_database/src/mutation/word_category.rs b/kolomoni_database/src/mutation/word_category.rs deleted file mode 100644 index b75531f..0000000 --- a/kolomoni_database/src/mutation/word_category.rs +++ /dev/null @@ -1,48 +0,0 @@ -use miette::Result; -use miette::{Context, IntoDiagnostic}; -use sea_orm::{ActiveModelTrait, ActiveValue, ConnectionTrait, TransactionTrait}; -use uuid::Uuid; - -use crate::entities::word_category; - -pub struct WordCategoryMutation; - -impl WordCategoryMutation { - pub async fn add_category_to_word( - database: &C, - word_uuid: Uuid, - category_id: i32, - ) -> Result<()> { - let word_category_active_model = word_category::ActiveModel { - word_id: ActiveValue::Set(word_uuid), - category_id: ActiveValue::Set(category_id), - }; - - word_category_active_model - .insert(database) - .await - .into_diagnostic() - .wrap_err("Failed while inserting word category relationship.")?; - - Ok(()) - } - - pub async fn remove_category_from_word( - database: &C, - word_uuid: Uuid, - category_id: i32, - ) -> Result<()> { - let word_category_active_model = word_category::ActiveModel { - word_id: ActiveValue::Unchanged(word_uuid), - category_id: ActiveValue::Unchanged(category_id), - }; - - word_category_active_model - .delete(database) - .await - .into_diagnostic() - .wrap_err("Failed while deleting word category relationship.")?; - - Ok(()) - } -} diff --git a/kolomoni_database/src/mutation/word_english.rs b/kolomoni_database/src/mutation/word_english.rs deleted file mode 100644 index 26b9437..0000000 --- a/kolomoni_database/src/mutation/word_english.rs +++ /dev/null @@ -1,140 +0,0 @@ -use chrono::{DateTime, Utc}; -use miette::{Context, IntoDiagnostic, Result}; -use sea_orm::{ActiveModelTrait, ActiveValue, ConnectionTrait, TransactionTrait, TryIntoModel}; -use uuid::Uuid; - -use crate::{ - begin_transaction, - entities::{word, word_english}, - shared::{generate_random_word_uuid, WordLanguage}, -}; - - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct NewEnglishWord { - pub lemma: String, - pub disambiguation: Option, - pub description: Option, -} - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct UpdatedEnglishWord { - pub lemma: Option, - pub disambiguation: Option, - pub description: Option, -} - - - -pub struct EnglishWordMutation; - -impl EnglishWordMutation { - pub async fn create( - database: &C, - english_word: NewEnglishWord, - ) -> Result { - let transaction = begin_transaction!(database)?; - - let random_uuid = generate_random_word_uuid(); - let created_at = Utc::now(); - - - let active_word = word::ActiveModel { - id: ActiveValue::Set(random_uuid), - language: ActiveValue::Set(WordLanguage::English.to_ietf_language_tag().to_string()), - }; - - active_word - .insert(&transaction) - .await - .into_diagnostic() - .wrap_err("Failed while inserting base word.")?; - - - let active_english_word = word_english::ActiveModel { - word_id: ActiveValue::Set(random_uuid), - lemma: ActiveValue::Set(english_word.lemma), - disambiguation: ActiveValue::Set(english_word.disambiguation), - description: ActiveValue::Set(english_word.description), - created_at: ActiveValue::Set(created_at.fixed_offset()), - last_modified_at: ActiveValue::Set(created_at.fixed_offset()), - }; - - let new_english_word = active_english_word - .insert(&transaction) - .await - .into_diagnostic() - .wrap_err("Failed while inserting english word.")?; - - - transaction - .commit() - .await - .into_diagnostic() - .wrap_err("Failed to commit english word creation transaction.")?; - - - Ok(new_english_word) - } - - pub async fn update( - database: &C, - word_uuid: Uuid, - update: UpdatedEnglishWord, - ) -> Result { - let mut active_word_model = word_english::ActiveModel { - word_id: ActiveValue::Unchanged(word_uuid), - last_modified_at: ActiveValue::Set(Utc::now().fixed_offset()), - ..Default::default() - }; - - if let Some(updated_lemma) = update.lemma { - active_word_model.lemma = ActiveValue::Set(updated_lemma); - }; - - if let Some(updated_disambiguation) = update.disambiguation { - active_word_model.disambiguation = ActiveValue::Set(Some(updated_disambiguation)); - } - - if let Some(updated_description) = update.description { - active_word_model.description = ActiveValue::Set(Some(updated_description)); - } - - let updated_active_word = active_word_model - .save(database) - .await - .into_diagnostic() - .wrap_err("Failed to update english word.")?; - - let updated_word = updated_active_word - .try_into_model() - .into_diagnostic() - .wrap_err("Failed to convert active english model to normal model.")?; - - - Ok(updated_word) - } - - pub async fn set_last_modified_at( - database: &C, - word_uuid: Uuid, - new_last_edited_at: DateTime, - ) -> Result { - let active_word_model = word_english::ActiveModel { - word_id: ActiveValue::Unchanged(word_uuid), - last_modified_at: ActiveValue::Set(new_last_edited_at.fixed_offset()), - ..Default::default() - }; - - let updated_word = active_word_model - .update(database) - .await - .into_diagnostic() - .wrap_err("Failed while setting last modified datetime for english word.")?; - - - Ok(updated_word) - } - - // For deletion, see [`WordMutation::delete`][super::word::WordMutation::delete]. -} diff --git a/kolomoni_database/src/mutation/word_slovene.rs b/kolomoni_database/src/mutation/word_slovene.rs deleted file mode 100644 index cd1de79..0000000 --- a/kolomoni_database/src/mutation/word_slovene.rs +++ /dev/null @@ -1,139 +0,0 @@ -use chrono::{DateTime, Utc}; -use miette::{Context, IntoDiagnostic, Result}; -use sea_orm::{ActiveModelTrait, ActiveValue, ConnectionTrait, TransactionTrait, TryIntoModel}; -use uuid::Uuid; - -use crate::{ - begin_transaction, - entities::{word, word_slovene}, - shared::{generate_random_word_uuid, WordLanguage}, -}; - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct NewSloveneWord { - pub lemma: String, - pub disambiguation: Option, - pub description: Option, -} - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct UpdatedSloveneWord { - pub lemma: Option, - pub disambiguation: Option, - pub description: Option, -} - - - -pub struct SloveneWordMutation; - -impl SloveneWordMutation { - pub async fn create( - database: &C, - slovene_word: NewSloveneWord, - ) -> Result { - let transaction = begin_transaction!(database)?; - - let random_uuid = generate_random_word_uuid(); - let created_at = Utc::now(); - - - let active_word = word::ActiveModel { - id: ActiveValue::Set(random_uuid), - language: ActiveValue::Set(WordLanguage::Slovene.to_ietf_language_tag().to_string()), - }; - - active_word - .insert(&transaction) - .await - .into_diagnostic() - .wrap_err("Failed while inserting base word.")?; - - - let active_slovene_word = word_slovene::ActiveModel { - word_id: ActiveValue::Set(random_uuid), - lemma: ActiveValue::Set(slovene_word.lemma), - disambiguation: ActiveValue::Set(slovene_word.disambiguation), - description: ActiveValue::Set(slovene_word.description), - created_at: ActiveValue::Set(created_at.fixed_offset()), - last_modified_at: ActiveValue::Set(created_at.fixed_offset()), - }; - - let new_slovene_word = active_slovene_word - .insert(&transaction) - .await - .into_diagnostic() - .wrap_err("Failed while inserting slovene word.")?; - - - transaction - .commit() - .await - .into_diagnostic() - .wrap_err("Failed to commit english word creation transaction.")?; - - Ok(new_slovene_word) - } - - pub async fn update( - database: &C, - word_uuid: Uuid, - update: UpdatedSloveneWord, - ) -> Result { - let mut active_word_model = word_slovene::ActiveModel { - word_id: ActiveValue::Unchanged(word_uuid), - last_modified_at: ActiveValue::Set(Utc::now().fixed_offset()), - ..Default::default() - }; - - if let Some(updated_lemma) = update.lemma { - active_word_model.lemma = ActiveValue::Set(updated_lemma); - }; - - if let Some(updated_disambiguation) = update.disambiguation { - active_word_model.disambiguation = ActiveValue::Set(Some(updated_disambiguation)); - } - - if let Some(updated_description) = update.description { - active_word_model.description = ActiveValue::Set(Some(updated_description)); - } - - - let updated_active_word = active_word_model - .save(database) - .await - .into_diagnostic() - .wrap_err("Failed to update slovene word.")?; - - let updated_word = updated_active_word - .try_into_model() - .into_diagnostic() - .wrap_err("Failed to convert active slovene model to normal model.")?; - - - Ok(updated_word) - } - - pub async fn set_last_modified_at( - database: &C, - word_uuid: Uuid, - new_last_edited_at: DateTime, - ) -> Result { - let active_word_model = word_slovene::ActiveModel { - word_id: ActiveValue::Unchanged(word_uuid), - last_modified_at: ActiveValue::Set(new_last_edited_at.fixed_offset()), - ..Default::default() - }; - - let updated_word = active_word_model - .update(database) - .await - .into_diagnostic() - .wrap_err("Failed while setting last modified datetime for slovene word.")?; - - - Ok(updated_word) - } - - // For deletion, see [`WordMutation::delete`][super::word::WordMutation::delete]. -} diff --git a/kolomoni_database/src/mutation/word_translation.rs b/kolomoni_database/src/mutation/word_translation.rs deleted file mode 100644 index 68db024..0000000 --- a/kolomoni_database/src/mutation/word_translation.rs +++ /dev/null @@ -1,116 +0,0 @@ -use chrono::Utc; -use miette::{Context, IntoDiagnostic, Result}; -use sea_orm::{ActiveModelTrait, ActiveValue, ConnectionTrait, TransactionTrait}; -use uuid::Uuid; - -use super::{EnglishWordMutation, SloveneWordMutation}; -use crate::{begin_transaction, commit_transaction, entities::word_translation}; - - - -pub struct NewTranslation { - pub english_word_id: Uuid, - pub slovene_word_id: Uuid, -} - -pub struct TranslationToDelete { - pub english_word_id: Uuid, - pub slovene_word_id: Uuid, -} - - -pub struct TranslationMutation; - -impl TranslationMutation { - pub async fn create( - database: &C, - new_translation: NewTranslation, - ) -> Result { - let transaction = begin_transaction!(database)?; - - - let active_translation = word_translation::ActiveModel { - english_word_id: ActiveValue::Set(new_translation.english_word_id), - slovene_word_id: ActiveValue::Set(new_translation.slovene_word_id), - translated_at: ActiveValue::Set(Utc::now().fixed_offset()), - }; - - let new_translation_model = active_translation - .insert(&transaction) - .await - .into_diagnostic() - .wrap_err("Failed while inserting new translation into the database.")?; - - - - // Now update the `last_modified_at` values for both words as well. - - let new_last_modified_at = Utc::now(); - - EnglishWordMutation::set_last_modified_at( - &transaction, - new_translation.english_word_id, - new_last_modified_at, - ) - .await - .wrap_err("Failed to set last modified for english word after creating a translation.")?; - - SloveneWordMutation::set_last_modified_at( - &transaction, - new_translation.slovene_word_id, - new_last_modified_at, - ) - .await - .wrap_err("Failed to set last modified for slovene word after creating a translation.")?; - - - commit_transaction!(transaction)?; - Ok(new_translation_model) - } - - pub async fn delete( - database: &C, - to_delete: TranslationToDelete, - ) -> Result<()> { - let transaction = begin_transaction!(database)?; - - - let active_translation = word_translation::ActiveModel { - english_word_id: ActiveValue::Unchanged(to_delete.english_word_id), - slovene_word_id: ActiveValue::Unchanged(to_delete.slovene_word_id), - ..Default::default() - }; - - - active_translation - .delete(&transaction) - .await - .into_diagnostic() - .wrap_err("Failed while deleting translation from the database.")?; - - - // Now update the `last_modified_at` values for both words as well. - - let new_last_modified_at = Utc::now(); - - EnglishWordMutation::set_last_modified_at( - &transaction, - to_delete.english_word_id, - new_last_modified_at, - ) - .await - .wrap_err("Failed to set last modified for english word after deleting a translation.")?; - - SloveneWordMutation::set_last_modified_at( - &transaction, - to_delete.slovene_word_id, - new_last_modified_at, - ) - .await - .wrap_err("Failed to set last modified for slovene word after deleting a translation.")?; - - - commit_transaction!(transaction)?; - Ok(()) - } -} diff --git a/kolomoni_database/src/mutation/word_translation_suggestion.rs b/kolomoni_database/src/mutation/word_translation_suggestion.rs deleted file mode 100644 index 0d618ca..0000000 --- a/kolomoni_database/src/mutation/word_translation_suggestion.rs +++ /dev/null @@ -1,118 +0,0 @@ -use chrono::Utc; -use miette::{Context, IntoDiagnostic, Result}; -use sea_orm::{ActiveModelTrait, ActiveValue, ConnectionTrait, TransactionTrait}; -use uuid::Uuid; - -use super::{EnglishWordMutation, SloveneWordMutation}; -use crate::{begin_transaction, commit_transaction, entities::word_translation_suggestion}; - - - -pub struct NewTranslationSuggestion { - pub english_word_id: Uuid, - pub slovene_word_id: Uuid, -} - -pub struct TranslationSuggestionToDelete { - pub english_word_id: Uuid, - pub slovene_word_id: Uuid, -} - - -pub struct TranslationSuggestionMutation; - -impl TranslationSuggestionMutation { - pub async fn create( - database: &C, - new_translation_suggestion: NewTranslationSuggestion, - ) -> Result { - let transaction = begin_transaction!(database)?; - - - let active_suggestion = word_translation_suggestion::ActiveModel { - english_word_id: ActiveValue::Set(new_translation_suggestion.english_word_id), - slovene_word_id: ActiveValue::Set(new_translation_suggestion.slovene_word_id), - suggested_at: ActiveValue::Set(Utc::now().fixed_offset()), - }; - - let new_suggestion_model = active_suggestion - .insert(&transaction) - .await - .into_diagnostic() - .wrap_err("Failed while inserting new translation suggestion into the database.")?; - - - - // Now update the `last_modified_at` values for both words as well. - - let new_last_modified_at = Utc::now(); - - EnglishWordMutation::set_last_modified_at( - &transaction, - new_translation_suggestion.english_word_id, - new_last_modified_at, - ) - .await - .wrap_err("Failed to set last modified for english word after creating a suggestion.")?; - - SloveneWordMutation::set_last_modified_at( - &transaction, - new_translation_suggestion.slovene_word_id, - new_last_modified_at, - ) - .await - .wrap_err("Failed to set last modified for slovene word after creating a suggestion.")?; - - - - commit_transaction!(transaction)?; - Ok(new_suggestion_model) - } - - pub async fn delete( - database: &C, - to_delete: TranslationSuggestionToDelete, - ) -> Result<()> { - let transaction = begin_transaction!(database)?; - - - let active_suggestion = word_translation_suggestion::ActiveModel { - english_word_id: ActiveValue::Unchanged(to_delete.english_word_id), - slovene_word_id: ActiveValue::Unchanged(to_delete.slovene_word_id), - ..Default::default() - }; - - - active_suggestion - .delete(&transaction) - .await - .into_diagnostic() - .wrap_err("Failed while deleting translation suggestion from the database.")?; - - - - // Now update the `last_modified_at` values for both words as well. - - let new_last_modified_at = Utc::now(); - - EnglishWordMutation::set_last_modified_at( - &transaction, - to_delete.english_word_id, - new_last_modified_at, - ) - .await - .wrap_err("Failed to set last modified for english word after deleting a suggestion.")?; - - SloveneWordMutation::set_last_modified_at( - &transaction, - to_delete.slovene_word_id, - new_last_modified_at, - ) - .await - .wrap_err("Failed to set last modified for slovene word after deleting a suggestion.")?; - - - commit_transaction!(transaction)?; - Ok(()) - } -} diff --git a/kolomoni_database/src/query.rs b/kolomoni_database/src/query.rs deleted file mode 100644 index 2e43473..0000000 --- a/kolomoni_database/src/query.rs +++ /dev/null @@ -1,19 +0,0 @@ -mod category; -mod user; -mod user_role; -mod word; -mod word_category; -mod word_english; -mod word_slovene; -mod word_translation; -mod word_translation_suggestion; - -pub use category::*; -pub use user::*; -pub use user_role::*; -pub use word::*; -pub use word_category::*; -pub use word_english::*; -pub use word_slovene::*; -pub use word_translation::*; -pub use word_translation_suggestion::*; diff --git a/kolomoni_database/src/query/category.rs b/kolomoni_database/src/query/category.rs deleted file mode 100644 index ba72551..0000000 --- a/kolomoni_database/src/query/category.rs +++ /dev/null @@ -1,125 +0,0 @@ -use chrono::{DateTime, Utc}; -use miette::{Context, IntoDiagnostic, Result}; -use sea_orm::{ - sea_query::Expr, - ColumnTrait, - ConnectionTrait, - EntityTrait, - FromQueryResult, - QueryFilter, - QuerySelect, - TransactionTrait, -}; - -use crate::entities::category; - - -#[derive(Clone, PartialEq, Eq, Debug, Default)] -pub struct CategoriesQueryOptions { - pub only_categories_modified_after: Option>, -} - - -pub struct CategoryQuery; - -impl CategoryQuery { - pub async fn exists_by_id( - database: &C, - category_id: i32, - ) -> Result { - #[derive(Debug, FromQueryResult, PartialEq, Eq, Hash)] - struct SuggestionCount { - count: i64, - } - - let mut select_query = category::Entity::find() - .filter(category::Column::Id.eq(category_id)) - .select_only(); - - select_query.expr_as(Expr::val(1).count(), "count"); - - let select_result = select_query - .into_model::() - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed whie looking up whether a category ID exists in the database.")?; - - - match select_result { - Some(count) => { - debug_assert!(count.count <= 1); - Ok(count.count == 1) - } - None => Ok(false), - } - } - - pub async fn exists_by_both_names( - database: &C, - slovene_name: String, - english_name: String, - ) -> Result { - #[derive(Debug, FromQueryResult, PartialEq, Eq, Hash)] - struct SuggestionCount { - count: i64, - } - - let mut select_query = category::Entity::find() - .filter(category::Column::SloveneName.eq(slovene_name)) - .filter(category::Column::EnglishName.eq(english_name)) - .select_only(); - - select_query.expr_as(Expr::val(1).count(), "count"); - - let select_result = select_query - .into_model::() - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed whie looking up whether a category name exists in the database.")?; - - - match select_result { - Some(count) => { - debug_assert!(count.count <= 1); - Ok(count.count == 1) - } - None => Ok(false), - } - } - - pub async fn get_by_id( - database: &C, - category_id: i32, - ) -> Result> { - let query = category::Entity::find_by_id(category_id) - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while fetching category from database.")?; - - Ok(query) - } - - pub async fn all( - database: &C, - options: CategoriesQueryOptions, - ) -> Result> { - let mut query = category::Entity::find(); - - if let Some(only_categories_modified_after) = options.only_categories_modified_after { - query = - query.filter(category::Column::LastModifiedAt.gt(only_categories_modified_after)); - } - - - let categories = query - .all(database) - .await - .into_diagnostic() - .wrap_err("Failed while fetching all categories from database.")?; - - Ok(categories) - } -} diff --git a/kolomoni_database/src/query/user.rs b/kolomoni_database/src/query/user.rs deleted file mode 100644 index 4090f4b..0000000 --- a/kolomoni_database/src/query/user.rs +++ /dev/null @@ -1,139 +0,0 @@ -use miette::{Context, IntoDiagnostic, Result}; -use sea_orm::sea_query::Expr; -use sea_orm::{ - ColumnTrait, - ConnectionTrait, - EntityTrait, - FromQueryResult, - QueryFilter, - QuerySelect, -}; - -use super::super::entities::prelude::User; -use super::super::entities::user; -use crate::mutation::ArgonHasher; - - -/// Queries related to the [`crate::entities::user::Entity`] entity. -pub struct UserQuery; - -impl UserQuery { - /// Get a user by their ID. - pub async fn get_user_by_id( - database: &C, - id: i32, - ) -> Result> { - User::find_by_id(id) - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while searching database for user by ID.") - } - - /// Get a user by their username. - pub async fn get_user_by_username( - database: &C, - username: &str, - ) -> Result> { - User::find() - .filter(user::Column::Username.eq(username)) - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while searching database for user by username.") - } - - pub async fn user_exists_by_user_id( - database: &C, - user_id: i32, - ) -> Result { - #[derive(Debug, FromQueryResult, PartialEq, Eq, Hash)] - struct UserExistenceCount { - count: i64, - } - - let mut user_exists_query = User::find().select_only(); - - user_exists_query.expr_as(Expr::val(1).count(), "count"); - - let count_result = user_exists_query - .filter(user::Column::Id.eq(user_id)) - .into_model::() - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while looking up whether the user exists by ID.")?; - - - match count_result { - Some(user_count) => Ok(user_count.count == 1), - None => Ok(false), - } - } - - /// Check whether a user exists (by their username). - pub async fn user_exists_by_username( - database: &C, - username: &str, - ) -> Result { - Ok(Self::get_user_by_username(database, username) - .await? - .is_some()) - } - - /// Check whether a user exists (by their display name). - pub async fn user_exists_by_display_name( - database: &C, - display_name: &str, - ) -> Result { - let user = User::find() - .filter(user::Column::DisplayName.eq(display_name)) - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while looking up whether user exists by username.")?; - - Ok(user.is_some()) - } - - /// Validate a user's credentials (the username and password combination). - /// This is basically the login verification method. - pub async fn validate_user_credentials( - database: &C, - hasher: &ArgonHasher, - username: &str, - password: &str, - ) -> Result> { - let user = User::find() - .filter(user::Column::Username.eq(username)) - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while looking up user in database (by username).")?; - - if let Some(user) = user { - let is_valid_password = hasher - .verify_password_against_hash(password, &user.hashed_password) - .wrap_err("Errored while validating password against hash.")?; - - if is_valid_password { - Ok(Some(user)) - } else { - Ok(None) - } - } else { - Ok(None) - } - } - - /// Get a list of all registered users. - pub async fn get_all_users(database: &C) -> Result> { - let users = User::find() - .all(database) - .await - .into_diagnostic() - .wrap_err("Failed while querying all users from database.")?; - - Ok(users) - } -} diff --git a/kolomoni_database/src/query/user_role.rs b/kolomoni_database/src/query/user_role.rs deleted file mode 100644 index 3e87888..0000000 --- a/kolomoni_database/src/query/user_role.rs +++ /dev/null @@ -1,177 +0,0 @@ -use std::collections::HashSet; - -use kolomoni_auth::{Permission, PermissionSet, Role, RoleSet}; -use miette::{miette, Result}; -use miette::{Context, IntoDiagnostic}; -use sea_orm::sea_query::Expr; -use sea_orm::{ - ColumnTrait, - ConnectionTrait, - EntityTrait, - FromQueryResult, - JoinType, - QueryFilter, - QuerySelect, -}; - -use crate::entities; - - -pub struct UserRoleQuery; - -impl UserRoleQuery { - pub async fn effective_user_permissions_from_user_id( - database: &C, - user_id: i32, - ) -> Result { - #[derive(Debug, FromQueryResult, PartialEq, Eq, Hash)] - struct PermissionIdSelect { - permission_id: i32, - } - - // Functionally equivalent to the following SQL query: - // SELECT DISTINCT role_permission.permission_id FROM role_permission - // INNER JOIN user_role ON user_role.role_id = role_permission.role_id - // WHERE user_role.user_id = ; - - let distinct_permission_ids = entities::role_permission::Entity::find() - .select_only() - .column(entities::role_permission::Column::PermissionId) - .distinct() - .join( - JoinType::InnerJoin, - entities::role_permission::Entity::belongs_to(entities::user_role::Entity) - .from(entities::role_permission::Column::RoleId) - .to(entities::user_role::Column::RoleId) - .into(), - ) - .filter(entities::user_role::Column::UserId.eq(user_id)) - .into_model::() - .all(database) - .await - .into_diagnostic() - .wrap_err("Failed while looking up aggregated permission list.")?; - - - - if distinct_permission_ids.is_empty() { - return Ok(PermissionSet::new_empty()); - } - - let permission_id_set = distinct_permission_ids - .into_iter() - .map(|permission_struct| permission_struct.permission_id) - .collect::>(); - - let parsed_permission_set = permission_id_set - .into_iter() - .map(|permission_id| { - Permission::from_id(permission_id) - .ok_or_else(|| miette!("Failed to deserialize permission from database: unrecognized permission id {permission_id}!")) - }) - .collect::, _>>()?; - - Ok(PermissionSet::from_permission_set( - parsed_permission_set, - )) - } - - pub async fn user_roles(database: &C, user_id: i32) -> Result { - #[derive(Debug, FromQueryResult, PartialEq, Eq, Hash)] - struct RoleIdSelect { - role_id: i32, - } - - let user_roles = entities::user_role::Entity::find() - .select_only() - .column(entities::user_role::Column::RoleId) - .filter(entities::user_role::Column::UserId.eq(user_id)) - .into_model::() - .all(database) - .await - .into_diagnostic() - .wrap_err("Failed to retrieve list of user roles.")?; - - - let role_set = user_roles - .into_iter() - .map(|select_result| select_result.role_id) - .map(|role_id| { - Role::from_id(role_id).ok_or_else(|| { - miette!( - "Failed to deserialize database response: unrecognized role ID {role_id}!" - ) - }) - }) - .collect::>>()?; - - Ok(RoleSet::from_role_set(role_set)) - } - - pub async fn user_has_permission( - database: &C, - user_id: i32, - permission: Permission, - ) -> Result { - #[derive(Debug, FromQueryResult, PartialEq, Eq, Hash)] - struct PermissionCheckCountResult { - count: i64, - } - - // Functionally equivalent to the following SQL query: - // SELECT COUNT(1) AS "count" FROM "role_permission" - // INNER JOIN "user_role" ON "role_permission"."role_id" = "user_role"."role_id" - // WHERE "user_role"."user_id" = AND "role_permission"."permission_id" = ; - - let mut count_query = entities::role_permission::Entity::find().select_only(); - - // TODO Revert back to normal chaining when SeaORM updates this method to take `self` instead of `&mut self` - // (is marked as FIXME in their source). - count_query.expr_as(Expr::val(1).count(), "count"); - - let count_result = count_query - .join( - JoinType::InnerJoin, - entities::role_permission::Entity::belongs_to(entities::user_role::Entity) - .from(entities::role_permission::Column::RoleId) - .to(entities::user_role::Column::RoleId) - .into(), - ) - .filter(entities::user_role::Column::UserId.eq(user_id)) - .filter(entities::role_permission::Column::PermissionId.eq(permission.id())) - .into_model::() - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while looking up whether the user has a permission.")?; - - - match count_result { - Some(check) => Ok(check.count == 1), - None => Ok(false), - } - } -} - - -#[allow(async_fn_in_trait)] -pub trait UserPermissionsExt { - async fn from_database_by_user_id( - database: &C, - user_id: i32, - ) -> Result - where - Self: Sized; -} - -impl UserPermissionsExt for PermissionSet { - async fn from_database_by_user_id(database: &C, user_id: i32) -> Result - where - Self: Sized, - { - let permission_set = - UserRoleQuery::effective_user_permissions_from_user_id(database, user_id).await?; - - Ok(permission_set) - } -} diff --git a/kolomoni_database/src/query/word.rs b/kolomoni_database/src/query/word.rs deleted file mode 100644 index 243eeca..0000000 --- a/kolomoni_database/src/query/word.rs +++ /dev/null @@ -1,57 +0,0 @@ -use miette::{Context, IntoDiagnostic, Result}; -use sea_orm::{ - sea_query::Expr, - ColumnTrait, - ConnectionTrait, - EntityTrait, - FromQueryResult, - QueryFilter, - QuerySelect, - TransactionTrait, -}; -use uuid::Uuid; - -use crate::entities::{self, word}; - -pub struct WordQuery; - -impl WordQuery { - pub async fn get_by_uuid( - database: &C, - word_uuid: Uuid, - ) -> Result> { - word::Entity::find_by_id(word_uuid) - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while looking up base word by UUID.") - } - - pub async fn exists_by_uuid( - database: &C, - word_uuid: Uuid, - ) -> Result { - #[derive(Debug, FromQueryResult, PartialEq, Eq, Hash)] - struct CountResult { - count: i64, - } - - let mut query = word::Entity::find().select_only(); - - query.expr_as(Expr::val(1).count(), "count"); - - let count_result = query - .filter(word::Column::Id.eq(word_uuid)) - .into_model::() - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while looking up whether a word exists by UUID.")?; - - - match count_result { - Some(count) => Ok(count.count == 1), - None => Ok(false), - } - } -} diff --git a/kolomoni_database/src/query/word_category.rs b/kolomoni_database/src/query/word_category.rs deleted file mode 100644 index 2d5bdce..0000000 --- a/kolomoni_database/src/query/word_category.rs +++ /dev/null @@ -1,70 +0,0 @@ -use miette::{Context, IntoDiagnostic, Result}; -use sea_orm::{ - sea_query::Expr, - ColumnTrait, - ConnectionTrait, - EntityTrait, - FromQueryResult, - JoinType, - QueryFilter, - QuerySelect, - TransactionTrait, -}; -use uuid::Uuid; - -use crate::entities::{category, word_category}; - -pub struct WordCategoryQuery; - -impl WordCategoryQuery { - pub async fn word_categories_by_word_uuid( - database: &C, - word_uuid: Uuid, - ) -> Result> { - let select_query = category::Entity::find() - .join( - JoinType::InnerJoin, - category::Entity::belongs_to(word_category::Entity) - .from(category::Column::Id) - .to(word_category::Column::CategoryId) - .into(), - ) - .filter(word_category::Column::WordId.eq(word_uuid)) - .all(database) - .await - .into_diagnostic() - .wrap_err("Failed while looking up word categories by word UUID.")?; - - Ok(select_query) - } - - pub async fn word_has_category( - database: &C, - word_uuid: Uuid, - category_id: i32, - ) -> Result { - #[derive(Debug, FromQueryResult, PartialEq, Eq, Hash)] - struct CountResult { - count: i64, - } - - let mut query = word_category::Entity::find().select_only(); - - query.expr_as(Expr::val(1).count(), "count"); - - let count_result = query - .filter(word_category::Column::WordId.eq(word_uuid)) - .filter(word_category::Column::CategoryId.eq(category_id)) - .into_model::() - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while looking up whether a word has a category.")?; - - - match count_result { - Some(count) => Ok(count.count == 1), - None => Ok(false), - } - } -} diff --git a/kolomoni_database/src/query/word_english.rs b/kolomoni_database/src/query/word_english.rs deleted file mode 100644 index fc284fd..0000000 --- a/kolomoni_database/src/query/word_english.rs +++ /dev/null @@ -1,292 +0,0 @@ -use chrono::{DateTime, Utc}; -use miette::Result; -use miette::{Context, IntoDiagnostic}; -use sea_orm::sea_query::Expr; -use sea_orm::{ - ColumnTrait, - ConnectionTrait, - EntityTrait, - FromQueryResult, - QueryFilter, - QuerySelect, - TransactionTrait, -}; -use uuid::Uuid; - -use super::super::entities::prelude::WordEnglish; -use super::{ - ExpandedSloveneWordInfo, - TranslationQuery, - TranslationSuggestionQuery, - WordCategoryQuery, -}; -use crate::entities::{category, word_english}; - - -#[derive(Clone, PartialEq, Eq, Debug, Default)] -pub struct EnglishWordsQueryOptions { - pub only_words_modified_after: Option>, -} - - -pub struct RelatedEnglishWordInfo { - pub categories: Vec, - pub suggested_translations: Vec, - pub translations: Vec, -} - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct ExpandedEnglishWordInfo { - pub word: word_english::Model, - pub categories: Vec, - pub suggested_translations: Vec, - pub translations: Vec, -} - - - -pub struct EnglishWordQuery; - -impl EnglishWordQuery { - pub async fn word_exists_by_uuid( - database: &C, - word_uuid: Uuid, - ) -> Result { - #[derive(Debug, FromQueryResult, PartialEq, Eq, Hash)] - struct WordCount { - count: i64, - } - - let mut word_exists_query = word_english::Entity::find().select_only(); - - word_exists_query.expr_as(Expr::val(1).count(), "count"); - - let count_result = word_exists_query - .filter(word_english::Column::WordId.eq(word_uuid)) - .into_model::() - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while looking up whether the english word exists by uuid.")?; - - match count_result { - Some(word_count) => { - debug_assert!(word_count.count <= 1); - Ok(word_count.count == 1) - } - None => Ok(false), - } - } - - pub async fn word_exists_by_lemma( - database: &C, - lemma: String, - ) -> Result { - #[derive(Debug, FromQueryResult, PartialEq, Eq, Hash)] - struct WordCount { - count: i64, - } - - let mut word_exists_query = word_english::Entity::find().select_only(); - - word_exists_query.expr_as(Expr::val(1).count(), "count"); - - let count_result = word_exists_query - .filter(word_english::Column::Lemma.eq(lemma)) - .into_model::() - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while looking up whether the english word exists by lemma.")?; - - match count_result { - Some(word_count) => { - debug_assert!(word_count.count <= 1); - Ok(word_count.count == 1) - } - None => Ok(false), - } - } - - pub async fn word_by_uuid( - database: &C, - word_uuid: Uuid, - ) -> Result> { - WordEnglish::find_by_id(word_uuid) - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while searching database for english word by UUID.") - } - - pub async fn word_by_lemma( - database: &C, - word_lemma: String, - ) -> Result> { - WordEnglish::find() - .filter(word_english::Column::Lemma.eq(word_lemma)) - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while searching database for english word by lemma.") - } - - pub async fn all_words( - database: &C, - options: EnglishWordsQueryOptions, - ) -> Result> { - let mut query = WordEnglish::find(); - - - // Add modifiers onto the query based on `options`. - if let Some(only_words_modified_after) = options.only_words_modified_after { - query = query.filter(word_english::Column::LastModifiedAt.gt(only_words_modified_after)); - } - - - query - .all(database) - .await - .into_diagnostic() - .wrap_err("Failed while querying all english words from the database.") - } - - pub async fn all_words_expanded( - database: &C, - options: EnglishWordsQueryOptions, - ) -> Result> { - let mut query = WordEnglish::find(); - - - // Add modifiers onto the query based on `options`. - if let Some(only_words_modified_after) = options.only_words_modified_after { - query = query.filter(word_english::Column::LastModifiedAt.gt(only_words_modified_after)); - } - - - let base_words = query - .all(database) - .await - .into_diagnostic() - .wrap_err("Failed while querying all english words from the database.")?; - - - // PERF This could be improved. - - let mut expanded_english_words = Vec::with_capacity(base_words.len()); - - for base_english_word in base_words { - let related_info = - Self::related_word_information_only(database, base_english_word.word_id).await?; - - expanded_english_words.push(ExpandedEnglishWordInfo { - word: base_english_word, - categories: related_info.categories, - suggested_translations: related_info.suggested_translations, - translations: related_info.translations, - }); - } - - Ok(expanded_english_words) - } - - pub async fn expanded_word_by_uuid( - database: &C, - word_uuid: Uuid, - ) -> Result> { - let Some(word_model) = Self::word_by_uuid(database, word_uuid).await? else { - return Ok(None); - }; - - let related_info = Self::related_word_information_only(database, word_uuid).await?; - - Ok(Some(ExpandedEnglishWordInfo { - word: word_model, - categories: related_info.categories, - suggested_translations: related_info.suggested_translations, - translations: related_info.translations, - })) - } - - pub async fn expanded_word_by_lemma( - database: &C, - word_lemma: String, - ) -> Result> { - let Some(word_model) = Self::word_by_lemma(database, word_lemma).await? else { - return Ok(None); - }; - - let related_info = Self::related_word_information_only(database, word_model.word_id).await?; - - Ok(Some(ExpandedEnglishWordInfo { - word: word_model, - categories: related_info.categories, - suggested_translations: related_info.suggested_translations, - translations: related_info.translations, - })) - } - - /// PERF: This might be a good candidate for optimization, probably with caching. - pub async fn related_word_information_only( - database: &C, - word_uuid: Uuid, - ) -> Result { - let categories = - WordCategoryQuery::word_categories_by_word_uuid(database, word_uuid).await?; - - - let suggested_translations = { - let suggested_translation_models = - TranslationSuggestionQuery::suggestions_for_english_word(database, word_uuid) - .await?; - - - let mut suggested_translations = Vec::with_capacity(suggested_translation_models.len()); - for suggested_translation_model in suggested_translation_models { - let suggested_translation_word_categories = - WordCategoryQuery::word_categories_by_word_uuid( - database, - suggested_translation_model.word_id, - ) - .await?; - - suggested_translations.push(ExpandedSloveneWordInfo { - word: suggested_translation_model, - categories: suggested_translation_word_categories, - }); - } - - suggested_translations - }; - - - let translations = { - let translation_models = - TranslationQuery::translations_for_english_word(database, word_uuid).await?; - - - let mut translations = Vec::with_capacity(translation_models.len()); - for translation_model in translation_models { - let translated_word_categories = WordCategoryQuery::word_categories_by_word_uuid( - database, - translation_model.word_id, - ) - .await?; - - translations.push(ExpandedSloveneWordInfo { - word: translation_model, - categories: translated_word_categories, - }); - } - - translations - }; - - - Ok(RelatedEnglishWordInfo { - categories, - suggested_translations, - translations, - }) - } -} diff --git a/kolomoni_database/src/query/word_slovene.rs b/kolomoni_database/src/query/word_slovene.rs deleted file mode 100644 index fdbd0e5..0000000 --- a/kolomoni_database/src/query/word_slovene.rs +++ /dev/null @@ -1,238 +0,0 @@ -use chrono::{DateTime, Utc}; -use miette::{Context, IntoDiagnostic, Result}; -use sea_orm::{ - sea_query::Expr, - ColumnTrait, - ConnectionTrait, - EntityTrait, - FromQueryResult, - QueryFilter, - QuerySelect, - TransactionTrait, -}; -use uuid::Uuid; - -use super::{super::entities::prelude::WordSlovene, WordCategoryQuery}; -use crate::entities::{category, word_slovene}; - - -#[derive(Default)] -pub struct SloveneWordsQueryOptions { - pub only_words_modified_after: Option>, -} - - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct RelatedSloveneWordInfo { - pub categories: Vec, -} - - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct ExpandedSloveneWordInfo { - pub word: word_slovene::Model, - pub categories: Vec, -} - - - -pub struct SloveneWordQuery; - -impl SloveneWordQuery { - pub async fn word_exists_by_uuid( - database: &C, - word_uuid: Uuid, - ) -> Result { - #[derive(Debug, FromQueryResult, PartialEq, Eq, Hash)] - struct WordCount { - count: i64, - } - - let mut word_exists_query = word_slovene::Entity::find().select_only(); - - word_exists_query.expr_as(Expr::val(1).count(), "count"); - - let count_result = word_exists_query - .filter(word_slovene::Column::WordId.eq(word_uuid)) - .into_model::() - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while looking up whether the slovene word exists by uuid.")?; - - match count_result { - Some(word_count) => { - debug_assert!(word_count.count <= 1); - Ok(word_count.count == 1) - } - None => Ok(false), - } - } - - pub async fn word_exists_by_lemma( - database: &C, - lemma: String, - ) -> Result { - #[derive(Debug, FromQueryResult, PartialEq, Eq, Hash)] - struct WordCount { - count: i64, - } - - let mut word_exists_query = word_slovene::Entity::find().select_only(); - - word_exists_query.expr_as(Expr::val(1).count(), "count"); - - let count_result = word_exists_query - .filter(word_slovene::Column::Lemma.eq(lemma)) - .into_model::() - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while looking up whether the slovene word exists by lemma.")?; - - match count_result { - Some(word_count) => { - debug_assert!(word_count.count <= 1); - Ok(word_count.count == 1) - } - None => Ok(false), - } - } - - pub async fn word_by_uuid( - database: &C, - word_uuid: Uuid, - ) -> Result> { - WordSlovene::find_by_id(word_uuid) - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while searching database for slovene word by UUID.") - } - - pub async fn word_by_lemma( - database: &C, - word_lemma: String, - ) -> Result> { - WordSlovene::find() - .filter(word_slovene::Column::Lemma.eq(word_lemma)) - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while seaching database for slovene word by lemma.") - } - - pub async fn expanded_word_by_uuid( - database: &C, - word_uuid: Uuid, - ) -> Result> { - let optional_base_word = WordSlovene::find_by_id(word_uuid) - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while searching database for slovene word by UUID.")?; - - let Some(base_word) = optional_base_word else { - return Ok(None); - }; - - - let related_info = Self::related_word_information_only(database, word_uuid).await?; - - - Ok(Some(ExpandedSloveneWordInfo { - word: base_word, - categories: related_info.categories, - })) - } - - pub async fn expanded_word_by_lemma( - database: &C, - word_lemma: String, - ) -> Result> { - let optional_base_word = WordSlovene::find() - .filter(word_slovene::Column::Lemma.eq(word_lemma)) - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while seaching database for slovene word by lemma.")?; - - let Some(base_word) = optional_base_word else { - return Ok(None); - }; - - - let related_info = Self::related_word_information_only(database, base_word.word_id).await?; - - - Ok(Some(ExpandedSloveneWordInfo { - word: base_word, - categories: related_info.categories, - })) - } - - pub async fn all_words( - database: &C, - options: SloveneWordsQueryOptions, - ) -> Result> { - let mut query = WordSlovene::find(); - - // Add modifiers onto the query based on `options`. - if let Some(only_words_modified_after) = options.only_words_modified_after { - query = query.filter(word_slovene::Column::LastModifiedAt.gt(only_words_modified_after)); - } - - query - .all(database) - .await - .into_diagnostic() - .wrap_err("Failed while querying all slovene words from the database.") - } - - pub async fn all_words_expanded( - database: &C, - options: SloveneWordsQueryOptions, - ) -> Result> { - let mut query = WordSlovene::find(); - - // Add modifiers onto the query based on `options`. - if let Some(only_words_modified_after) = options.only_words_modified_after { - query = query.filter(word_slovene::Column::LastModifiedAt.gt(only_words_modified_after)); - } - - let base_words = query - .all(database) - .await - .into_diagnostic() - .wrap_err("Failed while querying all slovene words from the database.")?; - - - // PERF This could be improved (but the expanded english word has bigger issues). - - let mut expanded_slovene_words = Vec::with_capacity(base_words.len()); - - for base_slovene_word in base_words { - let related_info = - Self::related_word_information_only(database, base_slovene_word.word_id).await?; - - expanded_slovene_words.push(ExpandedSloveneWordInfo { - word: base_slovene_word, - categories: related_info.categories, - }); - } - - Ok(expanded_slovene_words) - } - - - /// PERF: This might be a good candidate for optimization, probably with caching. - pub async fn related_word_information_only( - database: &C, - word_uuid: Uuid, - ) -> Result { - let categories = - WordCategoryQuery::word_categories_by_word_uuid(database, word_uuid).await?; - - Ok(RelatedSloveneWordInfo { categories }) - } -} diff --git a/kolomoni_database/src/query/word_translation.rs b/kolomoni_database/src/query/word_translation.rs deleted file mode 100644 index 7f166dd..0000000 --- a/kolomoni_database/src/query/word_translation.rs +++ /dev/null @@ -1,69 +0,0 @@ -use miette::{Context, IntoDiagnostic, Result}; -use sea_orm::{ - sea_query::Expr, - ColumnTrait, - ConnectionTrait, - EntityTrait, - FromQueryResult, - QueryFilter, - QuerySelect, - TransactionTrait, -}; -use uuid::Uuid; - -use crate::entities::{word_slovene, word_translation}; - -pub struct TranslationQuery; - -impl TranslationQuery { - pub async fn translations_for_english_word( - database: &C, - english_word_uuid: Uuid, - ) -> Result> { - let translations_query = word_slovene::Entity::find() - .inner_join(word_translation::Entity) - .filter(word_translation::Column::EnglishWordId.eq(english_word_uuid)) - .all(database) - .await - .into_diagnostic() - .wrap_err("Failed while retrieving translations from database.")?; - - Ok(translations_query) - } - - pub async fn exists( - database: &C, - english_word_uuid: Uuid, - slovene_word_uuid: Uuid, - ) -> Result { - #[derive(Debug, FromQueryResult, PartialEq, Eq, Hash)] - struct TranslationCount { - count: i64, - } - - - let mut suggestions_query = word_slovene::Entity::find() - .inner_join(word_translation::Entity) - .filter(word_translation::Column::EnglishWordId.eq(english_word_uuid)) - .filter(word_translation::Column::SloveneWordId.eq(slovene_word_uuid)) - .select_only(); - - suggestions_query.expr_as(Expr::val(1).count(), "count"); - - let translation_count_result = suggestions_query - .into_model::() - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while looking whether a translation exists in database.")?; - - - match translation_count_result { - Some(translation_count) => { - debug_assert!(translation_count.count <= 1); - Ok(translation_count.count == 1) - } - None => Ok(false), - } - } -} diff --git a/kolomoni_database/src/query/word_translation_suggestion.rs b/kolomoni_database/src/query/word_translation_suggestion.rs deleted file mode 100644 index 0ed9839..0000000 --- a/kolomoni_database/src/query/word_translation_suggestion.rs +++ /dev/null @@ -1,69 +0,0 @@ -use miette::{Context, IntoDiagnostic, Result}; -use sea_orm::{ - sea_query::Expr, - ColumnTrait, - ConnectionTrait, - EntityTrait, - FromQueryResult, - QueryFilter, - QuerySelect, - TransactionTrait, -}; -use uuid::Uuid; - -use crate::entities::{word_slovene, word_translation_suggestion}; - -pub struct TranslationSuggestionQuery; - -impl TranslationSuggestionQuery { - pub async fn suggestions_for_english_word( - database: &C, - english_word_uuid: Uuid, - ) -> Result> { - let suggestions_query = word_slovene::Entity::find() - .inner_join(word_translation_suggestion::Entity) - .filter(word_translation_suggestion::Column::EnglishWordId.eq(english_word_uuid)) - .all(database) - .await - .into_diagnostic() - .wrap_err("Failed while retrieving suggested translations from database.")?; - - Ok(suggestions_query) - } - - pub async fn exists( - database: &C, - english_word_uuid: Uuid, - slovene_word_uuid: Uuid, - ) -> Result { - #[derive(Debug, FromQueryResult, PartialEq, Eq, Hash)] - struct SuggestionCount { - count: i64, - } - - - let mut suggestions_query = word_slovene::Entity::find() - .inner_join(word_translation_suggestion::Entity) - .filter(word_translation_suggestion::Column::EnglishWordId.eq(english_word_uuid)) - .filter(word_translation_suggestion::Column::SloveneWordId.eq(slovene_word_uuid)) - .select_only(); - - suggestions_query.expr_as(Expr::val(1).count(), "count"); - - let suggestion_count_result = suggestions_query - .into_model::() - .one(database) - .await - .into_diagnostic() - .wrap_err("Failed while looking whether a suggested translation exists in database.")?; - - - match suggestion_count_result { - Some(suggestion_count) => { - debug_assert!(suggestion_count.count <= 1); - Ok(suggestion_count.count == 1) - } - None => Ok(false), - } - } -} diff --git a/kolomoni_database/src/shared.rs b/kolomoni_database/src/shared.rs deleted file mode 100644 index c5bbaad..0000000 --- a/kolomoni_database/src/shared.rs +++ /dev/null @@ -1,39 +0,0 @@ -use thiserror::Error; -use uuid::{NoContext, Timestamp, Uuid}; - -#[derive(Error, Debug)] -pub enum WordLanguageError { - #[error("unrecognized language: {language}")] - UnrecognizedLanguage { language: String }, -} - - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum WordLanguage { - English, - Slovene, -} - -impl WordLanguage { - pub fn from_ietf_language_tag(language_tag: &str) -> Result { - match language_tag { - "en" => Ok(Self::English), - "si" => Ok(Self::Slovene), - _ => Err(WordLanguageError::UnrecognizedLanguage { - language: language_tag.to_string(), - }), - } - } - - pub fn to_ietf_language_tag(self) -> &'static str { - match self { - WordLanguage::English => "en", - WordLanguage::Slovene => "si", - } - } -} - -#[inline] -pub fn generate_random_word_uuid() -> Uuid { - Uuid::new_v7(Timestamp::now(NoContext)) -} diff --git a/kolomoni_migrations/migrations/M0002_set-up-tables/up.sql b/kolomoni_migrations/migrations/M0002_set-up-tables/up.sql index e50fc0e..7f696cd 100644 --- a/kolomoni_migrations/migrations/M0002_set-up-tables/up.sql +++ b/kolomoni_migrations/migrations/M0002_set-up-tables/up.sql @@ -11,29 +11,19 @@ -- In our case, the underscore separator should be a double underscore ("__") instead. ----- --- Create schema: kolomoni ----- --- CREATE SCHEMA kolomoni AUTHORIZATION CURRENT_USER; - - - ---- -- Create table: permission ---- CREATE TABLE kolomoni.permission ( id integer NOT NULL, - name_en text NOT NULL, - name_sl text NOT NULL, + key text NOT NULL, description_en text NOT NULL, description_sl text NOT NULL, CONSTRAINT pk__permission PRIMARY KEY (id), - CONSTRAINT unique__permission__name_en - UNIQUE (name_en), - CONSTRAINT unique__permission__name_sl - UNIQUE (name_sl) + CONSTRAINT unique__permission__key + UNIQUE (key) ); -- We set the fillfactor to 100 because permissions will be very rarely modified. @@ -45,13 +35,8 @@ CREATE INDEX index__permission__id WITH (FILLFACTOR = 100); -- We set the fillfactor to 100 because permissions will be very rarely modified. -CREATE INDEX index__permission__name_en - ON kolomoni.permission (name_en) - WITH (FILLFACTOR = 100); - --- We set the fillfactor to 100 because permissions will be very rarely modified. -CREATE INDEX index__permission__name_sl - ON kolomoni.permission (name_sl) +CREATE INDEX index__permission__key + ON kolomoni.permission (key) WITH (FILLFACTOR = 100); @@ -62,16 +47,13 @@ CREATE INDEX index__permission__name_sl ---- CREATE TABLE kolomoni.role ( id integer NOT NULL, - name_en text NOT NULL, - name_sl text NOT NULL, + key text NOT NULL, description_en text NOT NULL, description_sl text NOT NULL, CONSTRAINT pk__role PRIMARY KEY (id), - CONSTRAINT unique__role__name_en - UNIQUE (name_en), - CONSTRAINT unique__role__name_sl - UNIQUE (name_sl) + CONSTRAINT unique__role__key + UNIQUE (key) ); -- We set the fillfactor to 100 because roles will be very rarely modified. @@ -80,14 +62,10 @@ CREATE INDEX index__role__id WITH (FILLFACTOR = 100); -- We set the fillfactor to 100 because roles will be very rarely modified. -CREATE INDEX index__role__name_en - ON kolomoni.role (name_en) +CREATE INDEX index__role__key + ON kolomoni.role (key) WITH (FILLFACTOR = 100); --- We set the fillfactor to 100 because roles will be very rarely modified. -CREATE INDEX index__role__name_sl - ON kolomoni.role (name_sl) - WITH (FILLFACTOR = 100); diff --git a/kolomoni_migrations/migrations/M0003_seed-permissions-and-roles/down.rs b/kolomoni_migrations/migrations/M0003_seed-permissions-and-roles/down.rs index b00e551..8e6910c 100644 --- a/kolomoni_migrations/migrations/M0003_seed-permissions-and-roles/down.rs +++ b/kolomoni_migrations/migrations/M0003_seed-permissions-and-roles/down.rs @@ -11,7 +11,7 @@ pub async fn down(mut context: MigrationContext<'_>) -> Result<(), MigrationRoll for permission in StandardPermission::all_permissions() { sqlx::query("DELETE FROM kolomoni.permission WHERE id = $1") - .bind(permission.id()) + .bind(permission.internal_id()) .execute(&mut *database_connection) .await .map_err(|error| MigrationRollbackError::FailedToExecuteQuery { error })?; @@ -19,7 +19,7 @@ pub async fn down(mut context: MigrationContext<'_>) -> Result<(), MigrationRoll for role in StandardRole::all_roles() { sqlx::query("DELETE FROM kolomoni.role WHERE id = $1") - .bind(role.id()) + .bind(role.internal_id()) .execute(&mut *database_connection) .await .map_err(|error| MigrationRollbackError::FailedToExecuteQuery { error })?; diff --git a/kolomoni_migrations/migrations/M0003_seed-permissions-and-roles/permissions.rs b/kolomoni_migrations/migrations/M0003_seed-permissions-and-roles/permissions.rs index d32296e..04fc11e 100644 --- a/kolomoni_migrations/migrations/M0003_seed-permissions-and-roles/permissions.rs +++ b/kolomoni_migrations/migrations/M0003_seed-permissions-and-roles/permissions.rs @@ -18,8 +18,8 @@ pub enum StandardPermission { WordRead, WordUpdate, WordDelete, - SuggestionCreate, - SuggestionDelete, + // SuggestionCreate, + // SuggestionDelete, TranslationCreate, TranslationDelete, CategoryCreate, @@ -38,8 +38,8 @@ impl StandardPermission { Self::WordRead, Self::WordUpdate, Self::WordDelete, - Self::SuggestionCreate, - Self::SuggestionDelete, + // Self::SuggestionCreate, + // Self::SuggestionDelete, Self::TranslationCreate, Self::TranslationDelete, Self::CategoryCreate, @@ -48,7 +48,7 @@ impl StandardPermission { ] } - pub fn id(&self) -> i32 { + pub fn internal_id(&self) -> i32 { match self { StandardPermission::UserSelfRead => 1, StandardPermission::UserSelfWrite => 2, @@ -58,8 +58,8 @@ impl StandardPermission { StandardPermission::WordRead => 6, StandardPermission::WordUpdate => 7, StandardPermission::WordDelete => 8, - StandardPermission::SuggestionCreate => 9, - StandardPermission::SuggestionDelete => 10, + // StandardPermission::SuggestionCreate => 9, + // StandardPermission::SuggestionDelete => 10, StandardPermission::TranslationCreate => 11, StandardPermission::TranslationDelete => 12, StandardPermission::CategoryCreate => 13, @@ -68,7 +68,7 @@ impl StandardPermission { } } - pub fn name(&self) -> &'static str { + pub fn external_key(&self) -> &'static str { match self { StandardPermission::UserSelfRead => "user.self:read", StandardPermission::UserSelfWrite => "user.self:write", @@ -78,8 +78,8 @@ impl StandardPermission { StandardPermission::WordRead => "word:read", StandardPermission::WordUpdate => "word:update", StandardPermission::WordDelete => "word:delete", - StandardPermission::SuggestionCreate => "word.suggestion:create", - StandardPermission::SuggestionDelete => "word.suggestion:delete", + // StandardPermission::SuggestionCreate => "word.suggestion:create", + // StandardPermission::SuggestionDelete => "word.suggestion:delete", StandardPermission::TranslationCreate => "word.translation:create", StandardPermission::TranslationDelete => "word.translation:delete", StandardPermission::CategoryCreate => "category:create", @@ -89,38 +89,58 @@ impl StandardPermission { } #[rustfmt::skip] - pub fn description(&self) -> &'static str { + pub fn english_description(&self) -> &'static str { match self { StandardPermission::UserSelfRead => - "Allows the user to log in and view their account information.", + "Allows users to log in and view their account information.", StandardPermission::UserSelfWrite => - "Allows the user to update their account information.", + "Allows users to update their account information.", StandardPermission::UserAnyRead => - "Allows the user to view public account information of any other user.", + "Allows users to view public account information of any other user.", StandardPermission::UserAnyWrite => - "Allows the user to update account information of any other user.", + "Allows users to update account information of any other user.", StandardPermission::WordCreate => - "Allows the user to create words in the dictionary.", + "Allows users to create words in the dictionary.", StandardPermission::WordRead => - "Allows the user to read words in the dictionary.", + "Allows users to read words in the dictionary.", StandardPermission::WordUpdate => - "Allows the user to update existing words in the dictionary (but not delete them).", + "Allows users to update existing words in the dictionary (but not delete them).", StandardPermission::WordDelete => - "Allows the user to delete words from the dictionary.", - StandardPermission::SuggestionCreate => - "Allows the user to create a translation suggestion.", - StandardPermission::SuggestionDelete => - "Allows the user to remove a translation suggestion.", + "Allows users to delete words from the dictionary.", + // StandardPermission::SuggestionCreate => + // "Allows the user to create a translation suggestion.", + // StandardPermission::SuggestionDelete => + // "Allows the user to remove a translation suggestion.", StandardPermission::TranslationCreate => - "Allows the user to translate a word.", + "Allows users to translate a word.", StandardPermission::TranslationDelete => - "Allows the user to remove a word translation.", + "Allows users to remove a word translation.", StandardPermission::CategoryCreate => - "Allows the user to create a word category.", + "Allows users to create a word category.", StandardPermission::CategoryUpdate => - "Allows the user to update an existing word category.", + "Allows users to update an existing word category.", StandardPermission::CategoryDelete => - "Allows the user to delete a word category.", + "Allows users to delete a word category.", + } + } + + pub fn slovene_description(&self) -> &'static str { + match self { + StandardPermission::UserSelfRead => "Omogoča prijavo in ogled podrobnosti uporabniškega računa.", + StandardPermission::UserSelfWrite => "Omogoča spreminjanje podrobnosti uporabniškega računa.", + StandardPermission::UserAnyRead => "Omogoča ogled javnih podatkov ostalih uporabniških računov.", + StandardPermission::UserAnyWrite => "Omogoča spreminjanje podrobnosti ostalih uporabniških računov.", + StandardPermission::WordCreate => "Omogoča ustvarjanje novih besed in njenih pomenov v slovarju.", + StandardPermission::WordRead => "Omogoča branje obstoječih besed in povezanih pomenov v slovarju.", + StandardPermission::WordUpdate => "Omogoča spreminjanje podrobnosti obstoječih besed in povezanih pomenov v slovarju.", + StandardPermission::WordDelete => "Omogoča brisanje besed in besednih pomenov iz slovarja.", + // StandardPermission::SuggestionCreate => todo!(), + // StandardPermission::SuggestionDelete => todo!(), + StandardPermission::TranslationCreate => "Omogoča ustvarjanje prevodov.", + StandardPermission::TranslationDelete => "Omogoča brisanje prevodov.", + StandardPermission::CategoryCreate => "Omogoča ustvarjanje besednih kategorij.", + StandardPermission::CategoryUpdate => "Omogoča spreminjanje podrobnosti obstoječih besednih kategorij.", + StandardPermission::CategoryDelete => "Omogoča brisanje obstoječih besedilnih kategorij." } } } diff --git a/kolomoni_migrations/migrations/M0003_seed-permissions-and-roles/roles.rs b/kolomoni_migrations/migrations/M0003_seed-permissions-and-roles/roles.rs index 0f565d6..439b825 100644 --- a/kolomoni_migrations/migrations/M0003_seed-permissions-and-roles/roles.rs +++ b/kolomoni_migrations/migrations/M0003_seed-permissions-and-roles/roles.rs @@ -1,5 +1,6 @@ use super::permissions::StandardPermission; + /// This is the list of available roles. /// /// **IMPORTANT: When still in the unstable phase, this role list (or ones on related migrations) @@ -21,27 +22,35 @@ impl StandardRole { vec![Self::User, Self::Administrator] } - pub fn id(&self) -> i32 { + pub fn internal_id(&self) -> i32 { match self { StandardRole::User => 1, StandardRole::Administrator => 2, } } - pub fn name(&self) -> &'static str { + pub fn external_key(&self) -> &'static str { match self { StandardRole::User => "user", StandardRole::Administrator => "administrator", } } - #[rustfmt::skip] - pub fn description(&self) -> &'static str { + pub fn english_description(&self) -> &'static str { + match self { + StandardRole::User => "Normal user with read permissions.", + StandardRole::Administrator => { + "Powerful user with almost all permissions, including deletions." + } + } + } + + pub fn slovene_description(&self) -> &'static str { match self { - StandardRole::User => - "Normal user with most read permissions.", - StandardRole::Administrator => - "Administrator with almost all permission, including deletions.", + StandardRole::User => "Navaden_a uporabnica_k, ki si lahko ogleduje vsebino.", + StandardRole::Administrator => { + "Uporabnica_k s skoraj vsemi dovoljenji, vključno z možnostjo brisanja." + } } } @@ -52,14 +61,14 @@ impl StandardRole { StandardPermission::UserSelfWrite, StandardPermission::UserAnyRead, StandardPermission::WordRead, - StandardPermission::SuggestionCreate, + // StandardPermission::SuggestionCreate, ], StandardRole::Administrator => vec![ StandardPermission::UserAnyWrite, StandardPermission::WordCreate, StandardPermission::WordUpdate, StandardPermission::WordDelete, - StandardPermission::SuggestionDelete, + // StandardPermission::SuggestionDelete, StandardPermission::TranslationCreate, StandardPermission::TranslationDelete, StandardPermission::CategoryCreate, diff --git a/kolomoni_migrations/migrations/M0003_seed-permissions-and-roles/up.rs b/kolomoni_migrations/migrations/M0003_seed-permissions-and-roles/up.rs index 91b9b8a..6a6a08a 100644 --- a/kolomoni_migrations/migrations/M0003_seed-permissions-and-roles/up.rs +++ b/kolomoni_migrations/migrations/M0003_seed-permissions-and-roles/up.rs @@ -11,12 +11,13 @@ pub async fn up(mut context: MigrationContext<'_>) -> Result<(), MigrationApplyE for permission in StandardPermission::all_permissions() { sqlx::query( - "INSERT INTO kolomoni.permission (id, name, description) \ - VALUES ($1, $2, $3)", + "INSERT INTO kolomoni.permission (id, key, description_en, description_sl) \ + VALUES ($1, $2, $3, $4)", ) - .bind(permission.id()) - .bind(permission.name()) - .bind(permission.description()) + .bind(permission.internal_id()) + .bind(permission.external_key()) + .bind(permission.english_description()) + .bind(permission.slovene_description()) .execute(&mut *database_connection) .await .map_err(|error| MigrationApplyError::FailedToExecuteQuery { error })?; @@ -24,12 +25,13 @@ pub async fn up(mut context: MigrationContext<'_>) -> Result<(), MigrationApplyE for role in StandardRole::all_roles() { sqlx::query( - "INSERT INTO kolomoni.role (id, name, description) \ - VALUES ($1, $2, $3)", + "INSERT INTO kolomoni.role (id, key, description_en, description_sl) \ + VALUES ($1, $2, $3, $4)", ) - .bind(role.id()) - .bind(role.name()) - .bind(role.description()) + .bind(role.internal_id()) + .bind(role.external_key()) + .bind(role.english_description()) + .bind(role.slovene_description()) .execute(&mut *database_connection) .await .map_err(|error| MigrationApplyError::FailedToExecuteQuery { error })?; @@ -39,8 +41,8 @@ pub async fn up(mut context: MigrationContext<'_>) -> Result<(), MigrationApplyE "INSERT INTO kolomoni.role_permission (role_id, permission_id) \ VALUES ($1, $2)", ) - .bind(role.id()) - .bind(assigned_permission.id()) + .bind(role.internal_id()) + .bind(assigned_permission.internal_id()) .execute(&mut *database_connection) .await .map_err(|error| MigrationApplyError::FailedToExecuteQuery { error })?; diff --git a/kolomoni_migrations/src/cli.rs b/kolomoni_migrations/src/cli.rs index d1a1c26..c34c3ae 100644 --- a/kolomoni_migrations/src/cli.rs +++ b/kolomoni_migrations/src/cli.rs @@ -56,7 +56,7 @@ pub struct InitializeCommandArguments { #[arg( long = "migrations-directory", short = 'm', - help = "Path to the to-be-created \"migrations\" directory that will contain all database migrations \ + help = "Path to the to-be-created migrations directory that will contain all database migrations \ for Stari Kolomoni. This should generally be set to \"./kolomoni_migrations/migrations\" \ (relative to the repository root)." )] @@ -271,15 +271,6 @@ pub struct GenerateCommandArguments { #[derive(Args)] pub struct UpCommandArguments { - #[arg( - long = "migrations-directory", - short = 'm', - help = "Path to the to-be-created \"migrations\" directory that will contain all database migrations \ - for Stari Kolomoni. This should generally be set to \"./kolomoni_migrations/migrations\" \ - (relative to the repository root)." - )] - pub migrations_directory_path: PathBuf, - #[command(flatten)] pub database: DatabaseConnectionArgs, @@ -296,15 +287,6 @@ pub struct UpCommandArguments { #[derive(Args)] pub struct DownCommandArguments { - #[arg( - long = "migrations-directory", - short = 'm', - help = "Path to the to-be-created \"migrations\" directory that will contain all database migrations \ - for Stari Kolomoni. This should generally be set to \"./kolomoni_migrations/migrations\" \ - (relative to the repository root)." - )] - pub migrations_directory_path: PathBuf, - #[command(flatten)] pub database: DatabaseConnectionArgs, diff --git a/scripts/database/init-database.ps1 b/scripts/database/init-database.ps1 index 875f7a4..d481357 100644 --- a/scripts/database/init-database.ps1 +++ b/scripts/database/init-database.ps1 @@ -30,13 +30,15 @@ Invoke-Expression "$PostgresPgCtlBinary initdb -D `"$DatabaseDataDirectory`" -o Write-Log -Name "Initialization" -Content "`"initdb`" finished, temporarily starting server to set up roles." Invoke-Expression "$PostgresPgCtlBinary start -D `"$DatabaseDataDirectory`" -l `"$LogFilePath`"" -Write-Log -Name "Initialization" -Content "Creating user kolomon (password kolomon) and database kolomondb." + Write-Log -Name "Initialization" -Content "Warning: using bad password, don't use in production." -Color DarkRed -Invoke-Expression "$PostgresPsqlBinary -h localhost -U postgres -c `"CREATE ROLE kolomon with PASSWORD 'kolomon' LOGIN`"" -Invoke-Expression "$PostgresPsqlBinary -h localhost -U postgres -c `"CREATE DATABASE kolomondb WITH OWNER kolomon ENCODING UTF8`"" -Invoke-Expression "$PostgresPsqlBinary -h localhost -U postgres -c `"REVOKE CONNECT ON DATABASE kolomondb FROM PUBLIC`"" -Invoke-Expression "$PostgresPsqlBinary -h localhost -U postgres -c `"GRANT CONNECT ON DATABASE kolomondb TO postgres`"" -Invoke-Expression "$PostgresPsqlBinary -h localhost -U postgres -c `"GRANT CONNECT ON DATABASE kolomondb TO kolomon`"" +Write-Log -Name "Initialization" -Content "Creating database stari_kolomoni..." +# Invoke-Expression "$PostgresPsqlBinary -h localhost -U postgres -c `"CREATE ROLE kolomon with PASSWORD 'kolomon' LOGIN`"" +Invoke-Expression "$PostgresPsqlBinary -h localhost -U postgres -c `"CREATE DATABASE stari_kolomoni ENCODING UTF8`"" +# Invoke-Expression "$PostgresPsqlBinary -h localhost -U postgres -c `"REVOKE CONNECT ON DATABASE kolomondb FROM PUBLIC`"" +# Invoke-Expression "$PostgresPsqlBinary -h localhost -U postgres -c `"GRANT CONNECT ON DATABASE kolomondb TO postgres`"" +# Invoke-Expression "$PostgresPsqlBinary -h localhost -U postgres -c `"GRANT CONNECT ON DATABASE kolomondb TO kolomon`"" + Write-Log -Name "Initialization" -Content "Stopping PostgreSQL server." Invoke-Expression "$PostgresPgCtlBinary stop -D `"$DatabaseDataDirectory`""