From 9f7e6647d02c6d19bd8ef6745f6ddb356dafb15e Mon Sep 17 00:00:00 2001 From: Jorge Hermo Date: Tue, 22 Oct 2024 18:05:51 +0200 Subject: [PATCH 1/3] feat: store input_type and output_type in db --- ...7a50ba321fd66fb9527a14b114b8543c239ea.json | 39 ++++++++++ ...a8020a8e3962f944612f5f6c45465526bc869.json | 40 ----------- ...5d247ae11d35b55a3a0f20929b5a45199420c.json | 72 +++++++++++++++++++ ...0ea18c8b27e59d5423ec93e564f4fe57e55fc.json | 17 ----- .../server/migrations/20241006174524_init.sql | 7 +- crates/server/src/dtos/share_dto.rs | 35 ++++++++- crates/server/src/model/share.rs | 11 ++- crates/server/src/routes/shares.rs | 16 ++++- crates/server/src/services/share.rs | 31 ++++++-- 9 files changed, 195 insertions(+), 73 deletions(-) create mode 100644 .sqlx/query-00d28a800544f8fffbc2425a8667a50ba321fd66fb9527a14b114b8543c239ea.json delete mode 100644 .sqlx/query-1f3b991d71b9b471f1e20838335a8020a8e3962f944612f5f6c45465526bc869.json create mode 100644 .sqlx/query-275e9124cd57a634624ca1e95d25d247ae11d35b55a3a0f20929b5a45199420c.json delete mode 100644 .sqlx/query-5182ce15cba8d1cbe9e23a84fc10ea18c8b27e59d5423ec93e564f4fe57e55fc.json diff --git a/.sqlx/query-00d28a800544f8fffbc2425a8667a50ba321fd66fb9527a14b114b8543c239ea.json b/.sqlx/query-00d28a800544f8fffbc2425a8667a50ba321fd66fb9527a14b114b8543c239ea.json new file mode 100644 index 0000000..9c4505d --- /dev/null +++ b/.sqlx/query-00d28a800544f8fffbc2425a8667a50ba321fd66fb9527a14b114b8543c239ea.json @@ -0,0 +1,39 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO share\n (id, input_data, input_type, output_type, query, expires_at)\n VALUES ($1, $2, $3, $4, $5, $6)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + { + "Custom": { + "name": "data_type", + "kind": { + "Enum": [ + "json", + "yaml" + ] + } + } + }, + { + "Custom": { + "name": "data_type", + "kind": { + "Enum": [ + "json", + "yaml" + ] + } + } + }, + "Text", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "00d28a800544f8fffbc2425a8667a50ba321fd66fb9527a14b114b8543c239ea" +} diff --git a/.sqlx/query-1f3b991d71b9b471f1e20838335a8020a8e3962f944612f5f6c45465526bc869.json b/.sqlx/query-1f3b991d71b9b471f1e20838335a8020a8e3962f944612f5f6c45465526bc869.json deleted file mode 100644 index ff2a205..0000000 --- a/.sqlx/query-1f3b991d71b9b471f1e20838335a8020a8e3962f944612f5f6c45465526bc869.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT * FROM share WHERE id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "json", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "query", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "expires_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - false - ] - }, - "hash": "1f3b991d71b9b471f1e20838335a8020a8e3962f944612f5f6c45465526bc869" -} diff --git a/.sqlx/query-275e9124cd57a634624ca1e95d25d247ae11d35b55a3a0f20929b5a45199420c.json b/.sqlx/query-275e9124cd57a634624ca1e95d25d247ae11d35b55a3a0f20929b5a45199420c.json new file mode 100644 index 0000000..537ca98 --- /dev/null +++ b/.sqlx/query-275e9124cd57a634624ca1e95d25d247ae11d35b55a3a0f20929b5a45199420c.json @@ -0,0 +1,72 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, input_data, input_type as \"input_type: DataType\",\n output_type as \"output_type: DataType\", query, expires_at\n FROM share WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "input_data", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "input_type: DataType", + "type_info": { + "Custom": { + "name": "data_type", + "kind": { + "Enum": [ + "json", + "yaml" + ] + } + } + } + }, + { + "ordinal": 3, + "name": "output_type: DataType", + "type_info": { + "Custom": { + "name": "data_type", + "kind": { + "Enum": [ + "json", + "yaml" + ] + } + } + } + }, + { + "ordinal": 4, + "name": "query", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "expires_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "275e9124cd57a634624ca1e95d25d247ae11d35b55a3a0f20929b5a45199420c" +} diff --git a/.sqlx/query-5182ce15cba8d1cbe9e23a84fc10ea18c8b27e59d5423ec93e564f4fe57e55fc.json b/.sqlx/query-5182ce15cba8d1cbe9e23a84fc10ea18c8b27e59d5423ec93e564f4fe57e55fc.json deleted file mode 100644 index bc0dab7..0000000 --- a/.sqlx/query-5182ce15cba8d1cbe9e23a84fc10ea18c8b27e59d5423ec93e564f4fe57e55fc.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO share (id, json, query, expires_at) VALUES ($1, $2, $3, $4)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid", - "Text", - "Text", - "Timestamptz" - ] - }, - "nullable": [] - }, - "hash": "5182ce15cba8d1cbe9e23a84fc10ea18c8b27e59d5423ec93e564f4fe57e55fc" -} diff --git a/crates/server/migrations/20241006174524_init.sql b/crates/server/migrations/20241006174524_init.sql index 40a1456..c0a8ac1 100644 --- a/crates/server/migrations/20241006174524_init.sql +++ b/crates/server/migrations/20241006174524_init.sql @@ -1,7 +1,10 @@ --- Add migration script here +CREATE TYPE data_type AS ENUM('json','yaml'); + CREATE TABLE share ( id UUID PRIMARY KEY, - json text NOT NULL, + input_data text NOT NULL, + input_type data_type NOT NULL, + output_type data_type NOT NULL, query text NOT NULL, expires_at TIMESTAMPTZ NOT NULL ); diff --git a/crates/server/src/dtos/share_dto.rs b/crates/server/src/dtos/share_dto.rs index 74de010..879933d 100644 --- a/crates/server/src/dtos/share_dto.rs +++ b/crates/server/src/dtos/share_dto.rs @@ -1,13 +1,15 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::model::share::Share; +use crate::model::share::{DataType, Share}; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ShareDTO { pub id: Uuid, - pub json: String, + pub input_data: String, + pub input_type: DataTypeDTO, + pub output_type: DataTypeDTO, pub query: String, } @@ -15,8 +17,35 @@ impl From for ShareDTO { fn from(share: Share) -> Self { Self { id: share.id, - json: share.json, + input_data: share.input_data, + input_type: share.input_type.into(), + output_type: share.output_type.into(), query: share.query, } } } + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DataTypeDTO { + Json, + Yaml, +} + +impl From for DataTypeDTO { + fn from(data_type: DataType) -> Self { + match data_type { + DataType::Json => Self::Json, + DataType::Yaml => Self::Yaml, + } + } +} + +impl From for DataType { + fn from(data_type: DataTypeDTO) -> Self { + match data_type { + DataTypeDTO::Json => Self::Json, + DataTypeDTO::Yaml => Self::Yaml, + } + } +} diff --git a/crates/server/src/model/share.rs b/crates/server/src/model/share.rs index 64b10c9..d843da0 100644 --- a/crates/server/src/model/share.rs +++ b/crates/server/src/model/share.rs @@ -5,7 +5,16 @@ use uuid::Uuid; #[derive(Debug, Serialize, Deserialize)] pub struct Share { pub id: Uuid, - pub json: String, + pub input_data: String, + pub input_type: DataType, + pub output_type: DataType, pub query: String, pub expires_at: DateTime, } + +#[derive(Debug, Serialize, Deserialize, sqlx::Type)] +#[sqlx(type_name = "data_type", rename_all = "lowercase")] +pub enum DataType { + Json, + Yaml, +} diff --git a/crates/server/src/routes/shares.rs b/crates/server/src/routes/shares.rs index b0c4af0..4848a0c 100644 --- a/crates/server/src/routes/shares.rs +++ b/crates/server/src/routes/shares.rs @@ -1,4 +1,4 @@ -use crate::dtos::share_dto::ShareDTO; +use crate::dtos::share_dto::{DataTypeDTO, ShareDTO}; use crate::routes; use crate::services::share::GetShareError; use crate::{ @@ -21,7 +21,9 @@ pub const SHARES_CONTEXT: &str = "/shares"; #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct CreateShareRequest { - json: String, + input_data: String, + input_type: DataTypeDTO, + output_type: DataTypeDTO, query: String, expiration_time_secs: i64, } @@ -47,8 +49,16 @@ async fn create_share( State(share_service): State, Json(request): Json, ) -> impl IntoResponse { + // TODO: refactor CreateShareRequest into a model CreateShareRequest and a CreateShareRequestDTO + // so the create_share method does not contain that much attributes let create_result = share_service - .create_share(request.json, request.query, request.expiration_time_secs) + .create_share( + request.input_data, + request.input_type.into(), + request.output_type.into(), + request.query, + request.expiration_time_secs, + ) .await; let share_id = match create_result { diff --git a/crates/server/src/services/share.rs b/crates/server/src/services/share.rs index ea12633..4b1a289 100644 --- a/crates/server/src/services/share.rs +++ b/crates/server/src/services/share.rs @@ -2,7 +2,7 @@ use chrono::{Duration, Utc}; use sqlx::PgPool; use uuid::Uuid; -use crate::model::share::Share; +use crate::model::share::{DataType, Share}; #[derive(Debug, thiserror::Error)] pub enum GetShareError { @@ -38,10 +38,14 @@ impl ShareService { pub async fn create_share( &self, - json: String, + input_data: String, + input_type: DataType, + output_type: DataType, query: String, expiration_time_secs: i64, ) -> Result { + // TODO: accept a CreateShare as input and validate it with a validate_create_response internal + // function to split the validation logic if expiration_time_secs <= 0 || expiration_time_secs > self.max_expiration_time_secs { return Err(CreateShareError::InvalidExpirationTime { actual: expiration_time_secs, @@ -54,9 +58,13 @@ impl ShareService { let expires_at = now + Duration::seconds(expiration_time_secs); sqlx::query!( - "INSERT INTO share (id, json, query, expires_at) VALUES ($1, $2, $3, $4)", + "INSERT INTO share + (id, input_data, input_type, output_type, query, expires_at) + VALUES ($1, $2, $3, $4, $5, $6)", uuid, - json, + input_data, + input_type as DataType, + output_type as DataType, query, expires_at ) @@ -67,9 +75,18 @@ impl ShareService { } pub async fn get_share(&self, id: Uuid) -> Result { - let share = sqlx::query_as!(Share, "SELECT * FROM share WHERE id = $1", id) - .fetch_optional(&self.db_connection) - .await?; + // See https://docs.rs/sqlx/0.4.2/sqlx/macro.query.html#force-a-differentcustom-type + // and https://github.com/launchbadge/sqlx/issues/1004#issuecomment-764921020 for + // more information about the custom type syntax + let share = sqlx::query_as!( + Share, + r#"SELECT id, input_data, input_type as "input_type: DataType", + output_type as "output_type: DataType", query, expires_at + FROM share WHERE id = $1"#, + id + ) + .fetch_optional(&self.db_connection) + .await?; let now = Utc::now(); let share = share.filter(|share| share.expires_at >= now); From 92efebf4a6a1d553f905fe4776d0fa966c96b807 Mon Sep 17 00:00:00 2001 From: Jorge Hermo Date: Tue, 22 Oct 2024 18:07:36 +0200 Subject: [PATCH 2/3] fix: clippy lints --- crates/server/src/main.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index c86e6bd..925551e 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -23,7 +23,7 @@ async fn main() { let database_connections = env::var("DATABASE_CONNECTIONS") .map(|s| { s.parse() - .expect(&format!("DATABASE_CONNECTIONS must be a number. Got {s}")) + .unwrap_or_else(|_| panic!("DATABASE_CONNECTIONS must be a number. Got {s}")) }) .unwrap_or(5); let db_connection = PgPoolOptions::new() @@ -34,9 +34,9 @@ async fn main() { let max_share_expiration_time_secs = env::var("MAX_SHARE_EXPIRATION_TIME_SECS") .map(|s| { - s.parse().expect(&format!( - "MAX_SHARE_EXPIRATION_TIME_SECS must be a number. Got {s}" - )) + s.parse().unwrap_or_else(|_| { + panic!("MAX_SHARE_EXPIRATION_TIME_SECS must be a number. Got {s}") + }) }) .unwrap_or(24 * 7) * 60 @@ -55,7 +55,7 @@ async fn main() { let app = gq_server::app(db_connection, max_share_expiration_time_secs); let listener = tokio::net::TcpListener::bind(addr) .await - .expect(&format!("Failed to bind address {addr}")); + .unwrap_or_else(|_| panic!("Failed to bind address {addr}")); tracing::info!("Server started. Listening on {addr}"); axum::serve(listener, app) From e7569088a6f42e056b37f3faa176df9be79e258d Mon Sep 17 00:00:00 2001 From: Jorge Hermo Date: Tue, 22 Oct 2024 18:10:16 +0200 Subject: [PATCH 3/3] style: format migration --- crates/server/migrations/20241006174524_init.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/server/migrations/20241006174524_init.sql b/crates/server/migrations/20241006174524_init.sql index c0a8ac1..e9c5864 100644 --- a/crates/server/migrations/20241006174524_init.sql +++ b/crates/server/migrations/20241006174524_init.sql @@ -1,4 +1,4 @@ -CREATE TYPE data_type AS ENUM('json','yaml'); +CREATE TYPE data_type AS ENUM('json', 'yaml'); CREATE TABLE share ( id UUID PRIMARY KEY,