diff --git a/Cargo.lock b/Cargo.lock index 804ab56..0008361 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,6 +99,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -812,14 +818,17 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ + "android-tzdata", "iana-time-zone", - "num-integer", + "js-sys", "num-traits", "serde", + "time 0.1.45", + "wasm-bindgen", "winapi", ] @@ -835,9 +844,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc" +checksum = "b4ed2379f8603fa2b7509891660e802b88c70a79a6427a70abb5968054de2c28" dependencies = [ "clap_builder", "clap_derive", @@ -846,9 +855,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990" +checksum = "72394f3339a76daf211e57d4bcb374410f3965dcc606dd0e03738c7888766980" dependencies = [ "anstream", "anstyle", @@ -859,9 +868,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.3.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "191d9573962933b4027f932c600cd252ce27a8ad5979418fe78e43c07996f27b" +checksum = "59e9ef9a08ee1c0e1f2e162121665ac45ac3783b0f897db7244ae75ad9a8f65b" dependencies = [ "heck", "proc-macro2", @@ -985,10 +994,12 @@ dependencies = [ "async-sqlx-session", "async-trait", "axum", + "chrono", "clap", "como_core", "como_domain", "rand_core", + "serde_json", "sqlx 0.6.3", "tokio", "tracing", @@ -1030,7 +1041,7 @@ dependencies = [ "rand", "sha2 0.10.6", "subtle", - "time", + "time 0.3.21", "version_check", ] @@ -1047,7 +1058,7 @@ dependencies = [ "rand", "sha2 0.10.6", "subtle", - "time", + "time 0.3.21", "version_check", ] @@ -1619,7 +1630,7 @@ dependencies = [ "cfg-if 1.0.0", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -2153,12 +2164,12 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eebffdb73fe72e917997fad08bdbf31ac50b0fa91cec93e69a0662e4264d454c" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -2315,9 +2326,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" [[package]] name = "opaque-debug" @@ -2358,9 +2369,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.52" +version = "0.10.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" +checksum = "69b3f656a17a6cbc115b5c7a40c616947d213ba182135b014d6051b73ab6f019" dependencies = [ "bitflags", "cfg-if 1.0.0", @@ -2390,9 +2401,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.87" +version = "0.9.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" +checksum = "c2ce0f250f34a308dcfdbb351f511359857d4ed2134ba715a4eadd46e1ffd617" dependencies = [ "cc", "libc", @@ -3277,7 +3288,7 @@ dependencies = [ "num-bigint", "num-traits", "thiserror", - "time", + "time 0.3.21", ] [[package]] @@ -3430,6 +3441,7 @@ dependencies = [ "bitflags", "byteorder", "bytes", + "chrono", "crc 3.0.1", "crossbeam-queue", "dirs", @@ -3465,6 +3477,7 @@ dependencies = [ "sqlx-rt 0.6.3", "stringprep", "thiserror", + "time 0.3.21", "tokio-stream", "url", "uuid", @@ -3635,6 +3648,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "time" version = "0.3.21" @@ -4106,6 +4130,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -4432,9 +4462,9 @@ checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" [[package]] name = "zitadel" -version = "3.3.2" +version = "3.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f663404099529f467b6ee708371187191bd0ab94d4821ea2357a3fe74ce2b4c8" +checksum = "902f5f12d22c788c97d643110ff7c7e6a906049f96946e01835f360900f4549b" dependencies = [ "axum", "axum-extra", @@ -4446,7 +4476,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "time", + "time 0.3.21", "tokio", "tonic-build", ] diff --git a/Cargo.toml b/Cargo.toml index 5be0b67..b3dae3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "como_gql", "como_api", ] +resolver = "2" [workspace.dependencies] como_bin = { path = "./como_bin/" } @@ -34,7 +35,10 @@ sqlx = { version = "0.6.2", features = [ "migrate", "uuid", "offline", + "time", + "chrono", ] } +chrono = { version = "0.4.26", features = ["serde"] } tokio = { version = "1.28.2", features = ["full"] } diff --git a/como_api/src/controllers/graphql.rs b/como_api/src/controllers/graphql.rs index a6c441b..0057354 100644 --- a/como_api/src/controllers/graphql.rs +++ b/como_api/src/controllers/graphql.rs @@ -7,6 +7,8 @@ use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; use axum::response::Html; use axum::{http::StatusCode, response::IntoResponse, routing::get, Extension, Router}; +use como_domain::user::ContextUserExt; +use como_domain::Context; use como_gql::graphql::{ComoSchema, MutationRoot, QueryRoot}; use como_infrastructure::register::ServiceRegister; use tower::ServiceBuilder; @@ -32,7 +34,11 @@ pub async fn graphql_handler( req: GraphQLRequest, ) -> Result { let req = req.into_inner(); - let req = req.data(user.user); + let req = req.data(user.user.clone()); + + let context = Context::new(); + let context = context.set_user_id(user.user.id.clone()); + let req = req.data(context); Ok(schema.execute(req).await.into()) } diff --git a/como_core/src/items/mod.rs b/como_core/src/items/mod.rs index 3df4696..ad85673 100644 --- a/como_core/src/items/mod.rs +++ b/como_core/src/items/mod.rs @@ -8,14 +8,22 @@ use como_domain::{ responses::CreatedItemDto, ItemDto, }, - users::User, + Context, }; pub type DynItemService = Arc; #[async_trait] pub trait ItemService { - async fn add_item(&self, item: CreateItemDto, user: &User) -> anyhow::Result; - async fn get_item(&self, query: GetItemQuery, user: &User) -> anyhow::Result; - async fn get_items(&self, query: GetItemsQuery, user: &User) -> anyhow::Result>; + async fn add_item( + &self, + context: &Context, + item: CreateItemDto, + ) -> anyhow::Result; + async fn get_item(&self, context: &Context, query: GetItemQuery) -> anyhow::Result; + async fn get_items( + &self, + context: &Context, + query: GetItemsQuery, + ) -> anyhow::Result>; } diff --git a/como_core/src/projects/mod.rs b/como_core/src/projects/mod.rs index 3d128bd..ecdba1c 100644 --- a/como_core/src/projects/mod.rs +++ b/como_core/src/projects/mod.rs @@ -3,18 +3,22 @@ use std::sync::Arc; use async_trait::async_trait; use como_domain::{ projects::{mutation::CreateProjectMutation, queries::GetProjectQuery, ProjectDto}, - users::User, + Context, }; pub type DynProjectService = Arc; #[async_trait] pub trait ProjectService { - async fn get_project(&self, query: GetProjectQuery) -> anyhow::Result; - async fn get_projects(&self, user: &User) -> anyhow::Result>; + async fn get_project( + &self, + context: &Context, + query: GetProjectQuery, + ) -> anyhow::Result; + async fn get_projects(&self, context: &Context) -> anyhow::Result>; async fn create_project( &self, + context: &Context, name: CreateProjectMutation, - user: &User, ) -> anyhow::Result; } diff --git a/como_core/src/users/mod.rs b/como_core/src/users/mod.rs index f2b10de..4e18cc1 100644 --- a/como_core/src/users/mod.rs +++ b/como_core/src/users/mod.rs @@ -1,14 +1,21 @@ use std::sync::Arc; use async_trait::async_trait; +use como_domain::Context; pub type DynUserService = Arc; #[async_trait] pub trait UserService { - async fn add_user(&self, username: String, password: String) -> anyhow::Result; + async fn add_user( + &self, + context: &Context, + username: String, + password: String, + ) -> anyhow::Result; async fn validate_user( &self, + context: &Context, username: String, password: String, ) -> anyhow::Result>; diff --git a/como_domain/src/common/mod.rs b/como_domain/src/common/mod.rs new file mode 100644 index 0000000..9d3280d --- /dev/null +++ b/como_domain/src/common/mod.rs @@ -0,0 +1,37 @@ +pub mod user; + +use std::collections::BTreeMap; + +#[derive(Debug, Clone)] +pub struct Context { + values: BTreeMap, +} + +impl Context { + pub fn new() -> Self { + Self { + values: Default::default(), + } + } + + pub fn with_value(&self, key: impl Into, value: impl Into) -> Self { + let mut values = self.values.clone(); + + let _ = values.insert(key.into(), value.into()); + + Self { values } + } + + pub fn with_value_mut( + &mut self, + key: impl Into, + value: impl Into, + ) -> &mut Self { + self.values.insert(key.into(), value.into()); + self + } + + pub fn get(&self, key: impl AsRef) -> Option<&str> { + self.values.get(key.as_ref()).map(|s| s.as_str()) + } +} diff --git a/como_domain/src/common/user.rs b/como_domain/src/common/user.rs new file mode 100644 index 0000000..f0e9556 --- /dev/null +++ b/como_domain/src/common/user.rs @@ -0,0 +1,23 @@ +use crate::Context; + +pub trait ContextUserExt { + fn set_user_id(&self, user_id: impl Into) -> Context; + fn set_user_id_mut(&mut self, user_id: impl Into) -> &mut Context; + fn get_user_id(&self) -> Option; +} + +const USER_ID_KEY: &str = "user_id"; + +impl ContextUserExt for Context { + fn set_user_id(&self, user_id: impl Into) -> Context { + self.with_value(USER_ID_KEY, user_id) + } + + fn set_user_id_mut(&mut self, user_id: impl Into) -> &mut Context { + self.with_value_mut(USER_ID_KEY, user_id) + } + + fn get_user_id(&self) -> Option { + self.get(USER_ID_KEY).map(|s| s.to_string()) + } +} diff --git a/como_domain/src/lib.rs b/como_domain/src/lib.rs index 6823a88..03a8d0c 100644 --- a/como_domain/src/lib.rs +++ b/como_domain/src/lib.rs @@ -1,3 +1,6 @@ +pub mod common; pub mod item; pub mod projects; pub mod users; + +pub use common::*; diff --git a/como_gql/src/graphql.rs b/como_gql/src/graphql.rs index 14f9079..336652c 100644 --- a/como_gql/src/graphql.rs +++ b/como_gql/src/graphql.rs @@ -5,7 +5,7 @@ use como_domain::item::requests::CreateItemDto; use como_domain::projects::mutation::CreateProjectMutation; use como_domain::projects::queries::GetProjectQuery; use como_domain::projects::ProjectDto; -use como_domain::users::User; + use como_infrastructure::register::ServiceRegister; pub type ComoSchema = Schema; @@ -19,11 +19,14 @@ impl MutationRoot { ctx: &Context<'_>, item: CreateItemDto, ) -> anyhow::Result { - let user = ctx.data_unchecked::(); + let context = ctx.data_unchecked::(); let services_register = ctx.data_unchecked::(); - let created_item = services_register.item_service.add_item(item, user).await?; + let created_item = services_register + .item_service + .add_item(context, item) + .await?; Ok(CreatedItem { id: created_item.id, @@ -35,13 +38,13 @@ impl MutationRoot { ctx: &Context<'_>, request: CreateProjectMutation, ) -> anyhow::Result { - let user = ctx.data_unchecked::(); + let context = ctx.data_unchecked::(); let services_register = ctx.data_unchecked::(); let project = services_register .project_service - .create_project(request, user) + .create_project(context, request) .await?; Ok(project) @@ -53,12 +56,12 @@ pub struct QueryRoot; #[Object] impl QueryRoot { async fn get_item(&self, ctx: &Context<'_>, query: GetItemQuery) -> anyhow::Result { - let user = ctx.data_unchecked::(); + let context = ctx.data_unchecked::(); let item = ctx .data_unchecked::() .item_service - .get_item(query, user) + .get_item(context, query) .await?; Ok(Item::from(item)) @@ -69,12 +72,12 @@ impl QueryRoot { ctx: &Context<'_>, query: GetItemsQuery, ) -> anyhow::Result> { - let user = ctx.data_unchecked::(); + let context = ctx.data_unchecked::(); let items = ctx .data_unchecked::() .item_service - .get_items(query, user) + .get_items(context, query) .await?; Ok(items.iter().map(|i| Item::from(i.clone())).collect()) @@ -86,18 +89,20 @@ impl QueryRoot { ctx: &Context<'_>, query: GetProjectQuery, ) -> anyhow::Result { + let context = ctx.data_unchecked::(); + ctx.data_unchecked::() .project_service - .get_project(query) + .get_project(context, query) .await } async fn get_projects(&self, ctx: &Context<'_>) -> anyhow::Result> { - let user = ctx.data_unchecked::(); + let context = ctx.data_unchecked::(); ctx.data_unchecked::() .project_service - .get_projects(user) + .get_projects(context) .await } } diff --git a/como_gql/src/items.rs b/como_gql/src/items.rs index 85b4ef7..624daf3 100644 --- a/como_gql/src/items.rs +++ b/como_gql/src/items.rs @@ -2,7 +2,6 @@ use async_graphql::{Context, Object}; use como_domain::{ item::{queries::GetItemQuery, ItemDto, ItemState}, projects::queries::GetProjectQuery, - users::User, }; use como_infrastructure::register::ServiceRegister; use uuid::Uuid; @@ -16,12 +15,12 @@ pub struct CreatedItem { #[Object] impl CreatedItem { pub async fn item(&self, ctx: &Context<'_>) -> anyhow::Result { - let user = ctx.data_unchecked::(); + let context = ctx.data_unchecked::(); let item = ctx .data_unchecked::() .item_service - .get_item(GetItemQuery { item_id: self.id }, user) + .get_item(context, GetItemQuery { item_id: self.id }) .await?; Ok(item.into()) @@ -55,12 +54,16 @@ impl Item { } pub async fn project(&self, ctx: &Context<'_>) -> anyhow::Result { + let context = ctx.data_unchecked::(); let project = ctx .data_unchecked::() .project_service - .get_project(GetProjectQuery { - project_id: self.project_id, - }) + .get_project( + context, + GetProjectQuery { + project_id: self.project_id, + }, + ) .await?; Ok(project.into()) diff --git a/como_gql/src/projects.rs b/como_gql/src/projects.rs index c034536..ddd943b 100644 --- a/como_gql/src/projects.rs +++ b/como_gql/src/projects.rs @@ -1,6 +1,6 @@ use async_graphql::{Context, Object}; use como_domain::projects::ProjectDto; -use como_domain::users::User; + use como_infrastructure::register::ServiceRegister; use uuid::Uuid; @@ -22,16 +22,16 @@ impl Project { } async fn items(&self, ctx: &Context<'_>) -> anyhow::Result> { - let user = ctx.data_unchecked::(); + let context = ctx.data_unchecked::(); let items = ctx .data_unchecked::() .item_service .get_items( + context, como_domain::item::queries::GetItemsQuery { project_id: self.id, }, - user, ) .await? .iter() diff --git a/como_infrastructure/Cargo.toml b/como_infrastructure/Cargo.toml index 1aedaa9..a5302d1 100644 --- a/como_infrastructure/Cargo.toml +++ b/como_infrastructure/Cargo.toml @@ -15,6 +15,8 @@ async-trait.workspace = true uuid.workspace = true anyhow.workspace = true sqlx.workspace = true +chrono.workspace = true +serde_json.workspace = true async-sqlx-session.workspace = true diff --git a/como_infrastructure/migrations/20230528154039_with-project.sql b/como_infrastructure/migrations/20230528154039_with-project.sql new file mode 100644 index 0000000..7322c6f --- /dev/null +++ b/como_infrastructure/migrations/20230528154039_with-project.sql @@ -0,0 +1,10 @@ +-- Add migration script here + +CREATE TABLE IF NOT EXISTS projects ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name varchar not null, + description varchar default null, + user_id varchar not null, + created_at timestamp not null, + updated_at timestamp not null +); diff --git a/como_infrastructure/migrations/20230603152353_items.sql b/como_infrastructure/migrations/20230603152353_items.sql new file mode 100644 index 0000000..a5568be --- /dev/null +++ b/como_infrastructure/migrations/20230603152353_items.sql @@ -0,0 +1,17 @@ +-- Add migration script here + +create table if not exists items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title varchar not null, + description varchar default null, + state integer not null, + user_id varchar not null, + project_id UUID not null, + created_at timestamp not null, + updated_at timestamp not null, + CONSTRAINT fk_project + FOREIGN KEY(project_id) + REFERENCES projects(id) + ON DELETE CASCADE +) + diff --git a/como_infrastructure/migrations/20230604084033_alter_state_string.sql b/como_infrastructure/migrations/20230604084033_alter_state_string.sql new file mode 100644 index 0000000..8d3e85a --- /dev/null +++ b/como_infrastructure/migrations/20230604084033_alter_state_string.sql @@ -0,0 +1,4 @@ +-- Add migration script here + +ALTER TABLE items ALTER COLUMN state TYPE varchar(255); + diff --git a/como_infrastructure/src/configs/mod.rs b/como_infrastructure/src/configs/mod.rs index 8036530..42ebf3a 100644 --- a/como_infrastructure/src/configs/mod.rs +++ b/como_infrastructure/src/configs/mod.rs @@ -1,17 +1,27 @@ +use clap::ValueEnum; + #[derive(clap::Parser)] pub struct AppConfig { #[clap(long, env)] pub database_url: String, + #[clap(long, env, default_value = "postgres")] + pub database_type: DatabaseType, #[clap(long, env)] pub rust_log: String, #[clap(long, env)] pub token_secret: String, - #[clap(long, env)] + #[clap(long, env, default_value = "3001")] pub api_port: u32, - #[clap(long, env)] + #[clap(long, env, default_value = "true")] pub run_migrations: bool, - #[clap(long, env)] + #[clap(long, env, default_value = "false")] pub seed: bool, #[clap(long, env)] pub cors_origin: String, } + +#[derive(Clone, Debug, ValueEnum)] +pub enum DatabaseType { + Postgres, + InMemory, +} diff --git a/como_infrastructure/src/register.rs b/como_infrastructure/src/register.rs index 7a7c475..1811523 100644 --- a/como_infrastructure/src/register.rs +++ b/como_infrastructure/src/register.rs @@ -5,10 +5,11 @@ use como_core::{items::DynItemService, projects::DynProjectService, users::DynUs use tracing::log::info; use crate::{ - configs::AppConfig, + configs::{AppConfig, DatabaseType}, database::ConnectionPool, services::{ - item_service::MemoryItemService, project_service::MemoryProjectService, + item_service::{DefaultItemService, MemoryItemService}, + project_service::{DefaultProjectService, MemoryProjectService}, user_service::DefaultUserService, }, }; @@ -25,20 +26,42 @@ impl ServiceRegister { pub async fn new(pool: ConnectionPool, config: Arc) -> anyhow::Result { info!("creating services"); - let item_service = Arc::new(MemoryItemService::new()) as DynItemService; - let project_service = Arc::new(MemoryProjectService::new()) as DynProjectService; - let user_service = Arc::new(DefaultUserService::new(pool.clone())) as DynUserService; - let store = PostgresSessionStore::new(&config.database_url).await?; + let s = match config.database_type { + DatabaseType::Postgres => { + let item_service = + Arc::new(DefaultItemService::new(pool.clone())) as DynItemService; + let project_service = + Arc::new(DefaultProjectService::new(pool.clone())) as DynProjectService; + let user_service = + Arc::new(DefaultUserService::new(pool.clone())) as DynUserService; + let store = PostgresSessionStore::new(&config.database_url).await?; + store.migrate().await?; - store.migrate().await?; + Self { + item_service, + user_service, + project_service, + session_store: store, + } + } + DatabaseType::InMemory => { + let item_service = Arc::new(MemoryItemService::new()) as DynItemService; + let project_service = Arc::new(MemoryProjectService::new()) as DynProjectService; + let user_service = + Arc::new(DefaultUserService::new(pool.clone())) as DynUserService; + let store = PostgresSessionStore::new(&config.database_url).await?; + store.migrate().await?; + Self { + item_service, + user_service, + project_service, + session_store: store, + } + } + }; info!("services created succesfully"); - Ok(Self { - item_service, - user_service, - project_service, - session_store: store, - }) + Ok(s) } } diff --git a/como_infrastructure/src/services/item_service.rs b/como_infrastructure/src/services/item_service.rs index 3813cdc..11ec9b3 100644 --- a/como_infrastructure/src/services/item_service.rs +++ b/como_infrastructure/src/services/item_service.rs @@ -12,30 +12,114 @@ use como_domain::{ responses::CreatedItemDto, ItemDto, }, - users::User, + user::ContextUserExt, + Context, }; use uuid::Uuid; -pub struct DefaultItemService {} +use crate::database::ConnectionPool; + +pub struct DefaultItemService { + pool: ConnectionPool, +} impl DefaultItemService { - pub fn new() -> Self { - Self {} + pub fn new(connection_pool: ConnectionPool) -> Self { + Self { + pool: connection_pool, + } } } #[async_trait] impl ItemService for DefaultItemService { - async fn add_item(&self, _item: CreateItemDto, user: &User) -> anyhow::Result { - todo!() + async fn add_item( + &self, + context: &Context, + item: CreateItemDto, + ) -> anyhow::Result { + let state = serde_json::to_string(&como_domain::item::ItemState::Created {})?; + let user_id = context.get_user_id().ok_or(anyhow::anyhow!("no user id"))?; + + let rec = sqlx::query!( + r#" + INSERT INTO items (id, title, description, state, project_id, user_id, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, now(), now()) + RETURNING id, title, description, state, project_id + "#, + Uuid::new_v4(), + item.name, + item.description, + state, + item.project_id, + user_id, + ) + .fetch_one(&self.pool) + .await?; + + Ok(CreatedItemDto { + id: rec.id, + title: rec.title, + description: rec.description, + state: como_domain::item::ItemState::Created {}, + project_id: rec.project_id, + }) } - async fn get_item(&self, _query: GetItemQuery, user: &User) -> anyhow::Result { - todo!() + async fn get_item(&self, context: &Context, query: GetItemQuery) -> anyhow::Result { + let user_id = context.get_user_id().ok_or(anyhow::anyhow!("no user id"))?; + + let rec = sqlx::query!( + r#" + SELECT id, title, description, state, project_id + FROM items + WHERE id = $1 AND user_id = $2 + "#, + query.item_id, + user_id, + ) + .fetch_one(&self.pool) + .await?; + + Ok(ItemDto { + id: rec.id, + title: rec.title, + description: rec.description, + state: serde_json::from_str(&rec.state)?, + project_id: rec.project_id, + }) } - async fn get_items(&self, _query: GetItemsQuery, user: &User) -> anyhow::Result> { - todo!() + async fn get_items( + &self, + context: &Context, + query: GetItemsQuery, + ) -> anyhow::Result> { + let user_id = context.get_user_id().ok_or(anyhow::anyhow!("no user id"))?; + + let recs = sqlx::query!( + r#" + SELECT id, title, description, state, project_id + FROM items + WHERE user_id = $1 and project_id = $2 + LIMIT 500 + "#, + user_id, + query.project_id, + ) + .fetch_all(&self.pool) + .await?; + + Ok(recs + .into_iter() + .map(|rec| ItemDto { + id: rec.id, + title: rec.title, + description: rec.description, + state: serde_json::from_str(&rec.state).unwrap(), + project_id: rec.project_id, + }) + .collect()) } } @@ -55,8 +139,8 @@ impl MemoryItemService { impl ItemService for MemoryItemService { async fn add_item( &self, + _context: &Context, create_item: CreateItemDto, - user: &User, ) -> anyhow::Result { if let Ok(mut item_store) = self.item_store.lock() { let item = ItemDto { @@ -75,7 +159,7 @@ impl ItemService for MemoryItemService { } } - async fn get_item(&self, query: GetItemQuery, user: &User) -> anyhow::Result { + async fn get_item(&self, _context: &Context, query: GetItemQuery) -> anyhow::Result { if let Ok(item_store) = self.item_store.lock() { let item = item_store .get(&query.item_id.to_string()) @@ -86,7 +170,11 @@ impl ItemService for MemoryItemService { } } - async fn get_items(&self, _query: GetItemsQuery, user: &User) -> anyhow::Result> { + async fn get_items( + &self, + _context: &Context, + _query: GetItemsQuery, + ) -> anyhow::Result> { todo!() } } diff --git a/como_infrastructure/src/services/project_service.rs b/como_infrastructure/src/services/project_service.rs index d9cf1f8..c8442bd 100644 --- a/como_infrastructure/src/services/project_service.rs +++ b/como_infrastructure/src/services/project_service.rs @@ -4,32 +4,107 @@ use axum::async_trait; use como_core::projects::ProjectService; use como_domain::{ projects::{mutation::CreateProjectMutation, queries::GetProjectQuery, ProjectDto}, - users::User, + user::ContextUserExt, + Context, }; use tokio::sync::Mutex; -pub struct DefaultProjectService {} +use crate::database::ConnectionPool; + +pub struct DefaultProjectService { + pool: ConnectionPool, +} impl DefaultProjectService { - pub fn new() -> Self { - Self {} + pub fn new(connection_pool: ConnectionPool) -> Self { + Self { + pool: connection_pool, + } } } #[async_trait] impl ProjectService for DefaultProjectService { - async fn get_project(&self, _query: GetProjectQuery) -> anyhow::Result { - todo!() + async fn get_project( + &self, + context: &Context, + query: GetProjectQuery, + ) -> anyhow::Result { + let user_id = context.get_user_id().ok_or(anyhow::anyhow!("no user id"))?; + + let rec = sqlx::query!( + r#" + SELECT id, name, description, user_id + FROM projects + WHERE id = $1 and user_id = $2 + "#, + query.project_id, + &user_id + ) + .fetch_one(&self.pool) + .await?; + + Ok(ProjectDto { + id: rec.id, + name: rec.name, + description: rec.description, + user_id: rec.user_id, + }) } - async fn get_projects(&self, user: &User) -> anyhow::Result> { - todo!() + async fn get_projects(&self, context: &Context) -> anyhow::Result> { + let user_id = context.get_user_id().ok_or(anyhow::anyhow!("no user id"))?; + + let recs = sqlx::query!( + r#" + SELECT id, name, description, user_id + FROM projects + WHERE user_id = $1 + LIMIT 500 + "#, + &user_id + ) + .fetch_all(&self.pool) + .await?; + + Ok(recs + .into_iter() + .map(|rec| ProjectDto { + id: rec.id, + name: rec.name, + description: rec.description, + user_id: rec.user_id, + }) + .collect::<_>()) } async fn create_project( &self, - name: CreateProjectMutation, - user: &User, + context: &Context, + request: CreateProjectMutation, ) -> anyhow::Result { - todo!() + let user_id = context.get_user_id().ok_or(anyhow::anyhow!("no user id"))?; + + let rec = sqlx::query!( + r#" + INSERT INTO projects (id, name, description, user_id, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING id + "#, + uuid::Uuid::new_v4(), + request.name, + request.description, + &user_id, + chrono::Utc::now().naive_utc(), + chrono::Utc::now().naive_utc(), + ) + .fetch_one(&self.pool) + .await?; + + Ok(ProjectDto { + id: rec.id, + name: request.name, + description: request.description, + user_id: user_id.clone(), + }) } } @@ -47,35 +122,43 @@ impl MemoryProjectService { #[async_trait] impl ProjectService for MemoryProjectService { - async fn get_project(&self, query: GetProjectQuery) -> anyhow::Result { + async fn get_project( + &self, + _context: &Context, + query: GetProjectQuery, + ) -> anyhow::Result { let ps = self.project_store.lock().await; Ok(ps .get(&query.project_id.to_string()) .ok_or(anyhow::anyhow!("could not find project"))? .clone()) } - async fn get_projects(&self, user: &User) -> anyhow::Result> { + async fn get_projects(&self, context: &Context) -> anyhow::Result> { + let user_id = context.get_user_id().ok_or(anyhow::anyhow!("no user id"))?; + Ok(self .project_store .lock() .await .values() - .filter(|p| p.user_id == user.id) + .filter(|p| p.user_id == user_id) .cloned() .collect::<_>()) } async fn create_project( &self, + context: &Context, mutation: CreateProjectMutation, - user: &User, ) -> anyhow::Result { + let user_id = context.get_user_id().ok_or(anyhow::anyhow!("no user id"))?; + let mut ps = self.project_store.lock().await; let project = ProjectDto { id: uuid::Uuid::new_v4(), name: mutation.name, description: None, - user_id: user.id.clone(), + user_id, }; ps.insert(project.id.to_string(), project.clone()); diff --git a/como_infrastructure/src/services/user_service.rs b/como_infrastructure/src/services/user_service.rs index 33d3471..20f3c79 100644 --- a/como_infrastructure/src/services/user_service.rs +++ b/como_infrastructure/src/services/user_service.rs @@ -1,6 +1,7 @@ use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; use axum::async_trait; use como_core::users::UserService; +use como_domain::Context; use rand_core::OsRng; use crate::database::ConnectionPool; @@ -14,7 +15,7 @@ impl DefaultUserService { Self { pool } } - fn hash_password(&self, password: String) -> anyhow::Result { + fn hash_password(&self, _context: &Context, password: String) -> anyhow::Result { let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::default(); @@ -26,7 +27,12 @@ impl DefaultUserService { Ok(password_hash) } - fn validate_password(&self, password: String, hashed_password: String) -> anyhow::Result { + fn validate_password( + &self, + _context: &Context, + password: String, + hashed_password: String, + ) -> anyhow::Result { let argon2 = Argon2::default(); let parsed_hash = PasswordHash::new(&hashed_password).map_err(|e| anyhow::anyhow!(e))?; @@ -39,8 +45,13 @@ impl DefaultUserService { #[async_trait] impl UserService for DefaultUserService { - async fn add_user(&self, username: String, password: String) -> anyhow::Result { - let hashed_password = self.hash_password(password)?; + async fn add_user( + &self, + context: &Context, + username: String, + password: String, + ) -> anyhow::Result { + let hashed_password = self.hash_password(context, password)?; let rec = sqlx::query!( r#" @@ -59,6 +70,7 @@ impl UserService for DefaultUserService { async fn validate_user( &self, + context: &Context, username: String, password: String, ) -> anyhow::Result> { @@ -73,7 +85,7 @@ impl UserService for DefaultUserService { .await?; match rec { - Some(user) => match self.validate_password(password, user.password_hash)? { + Some(user) => match self.validate_password(context, password, user.password_hash)? { true => Ok(Some(user.id.to_string())), false => Ok(None), }, diff --git a/como_infrastructure/target/sqlx/como_infrastructure/query-4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce.json b/como_infrastructure/target/sqlx/como_infrastructure/query-4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce.json index c881b13..5fb3030 100644 --- a/como_infrastructure/target/sqlx/como_infrastructure/query-4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce.json +++ b/como_infrastructure/target/sqlx/como_infrastructure/query-4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce.json @@ -1,5 +1,5 @@ { - "query": "\n SELECT * from users\n where username=$1\n ", + "query": "\n SELECT id, title, description, state, project_id\n FROM items\n WHERE user_id = $1 and project_id = $2\n LIMIT 500\n ", "describe": { "columns": [ { @@ -9,25 +9,38 @@ }, { "ordinal": 1, - "name": "username", + "name": "title", "type_info": "Varchar" }, { "ordinal": 2, - "name": "password_hash", + "name": "description", "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "state", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "project_id", + "type_info": "Uuid" } ], "parameters": { "Left": [ - "Text" + "Text", + "Uuid" ] }, "nullable": [ false, false, + true, + false, false ] }, - "hash": "d3f222cf6c3d9816705426fdbed3b13cb575bb432eb1f33676c0b414e67aecaf" + "hash": "bd2407ffb9637afcff3ffe1101e7c1920b8cf0be423ab0313d14acc9c76e0f93" } \ No newline at end of file diff --git a/cuddle.yaml b/cuddle.yaml index 2c1ec78..047330e 100644 --- a/cuddle.yaml +++ b/cuddle.yaml @@ -17,3 +17,10 @@ scripts: type: shell migrate_como: type: shell + new_migration: + type: shell + args: + name: + type: "env" + key: "name" + diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..5d56faf --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/scripts/new_migration.sh b/scripts/new_migration.sh new file mode 100755 index 0000000..26acf15 --- /dev/null +++ b/scripts/new_migration.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +export $(cat .env | xargs) + +cargo sqlx migrate add --source como_infrastructure/migrations $name