Skip to content

Commit

Permalink
feat: expiration time implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
jorgehermo9 committed Oct 7, 2024
1 parent 720cdf1 commit d3e660f
Show file tree
Hide file tree
Showing 12 changed files with 239 additions and 27 deletions.
85 changes: 85 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/server/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
PORT=3000
DATABASE_URL=postgres://postgres:password@localhost:5432/db
DATABASE_CONNECTIONS=5
MAX_SHARE_EXPIRATION_TIME_SECS=60
10 changes: 9 additions & 1 deletion crates/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,12 @@ tokio = { version = "1.40.0", features = ["full"] }
serde.workspace = true
uuid.workspace = true
thiserror.workspace = true
sqlx = { version = "0.8.2", features = ["postgres", "runtime-tokio", "uuid"] }
sqlx = { version = "0.8.2", features = [
"postgres",
"runtime-tokio",
"uuid",
"chrono",
] }
chrono = { version = "0.4.38", features = ["serde"] }
http-serde = "2.1.1"
tracing = "0.1.40"
3 changes: 2 additions & 1 deletion crates/server/migrations/20241006174524_init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
CREATE TABLE share (
id UUID PRIMARY KEY,
json text NOT NULL,
query text NOT NULL
query text NOT NULL,
expires_at TIMESTAMPTZ NOT NULL
);
1 change: 1 addition & 0 deletions crates/server/src/dto.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod error_object;
30 changes: 30 additions & 0 deletions crates/server/src/dto/error_object.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use axum::{http::StatusCode, response::IntoResponse, Json};
use chrono::{DateTime, Utc};
use serde::Serialize;
use uuid::Uuid;

#[derive(Debug, Serialize)]
pub struct ErrorObject {
pub message: String,
#[serde(with = "http_serde::status_code")]
pub code: StatusCode,
pub timestamp: DateTime<Utc>,
pub trace_id: Uuid,
}

impl ErrorObject {
pub fn new(message: String, code: StatusCode) -> Self {
Self {
message,
code,
timestamp: Utc::now(),
trace_id: Uuid::now_v7(),
}
}
}

impl IntoResponse for ErrorObject {
fn into_response(self) -> axum::response::Response {
(self.code, Json(self)).into_response()
}
}
5 changes: 3 additions & 2 deletions crates/server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ use axum::Router;
use sqlx::PgPool;
use state::AppState;

pub mod dto;
pub mod model;
pub mod routes;
pub mod services;
pub mod state;

pub fn app(db_connection: PgPool) -> Router {
let app_state = AppState::new(db_connection);
pub fn app(db_connection: PgPool, max_share_expiration_time_secs: i64) -> Router {
let app_state = AppState::new(db_connection, max_share_expiration_time_secs);
routes::router(app_state)
}
15 changes: 13 additions & 2 deletions crates/server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ use sqlx::postgres::PgPoolOptions;

#[tokio::main]
async fn main() {
// TODO: configure tracing
let port = env::var("PORT").unwrap_or_else(|_| "3000".to_string());
let addr = SocketAddr::from(([0, 0, 0, 0], port.parse().unwrap()));

let database_url = env::var("DATABASE_URL").unwrap();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let database_connections = env::var("DATABASE_CONNECTIONS")
.map(|s| s.parse().unwrap())
.unwrap_or(5);
Expand All @@ -17,12 +18,22 @@ async fn main() {
.await
.unwrap();

let max_share_expiration_time_secs = env::var("MAX_SHARE_EXPIRATION_TIME_SECS")
.map(|s| s.parse().unwrap())
// Defaults to 1 week
.unwrap_or(24 * 7);

assert!(
max_share_expiration_time_secs > 0,
"MAX_SHARE_EXPIRATION_TIME_SECS must be > 0"
);

sqlx::migrate!("./migrations")
.run(&db_connection)
.await
.unwrap();

let app = gq_server::app(db_connection);
let app = gq_server::app(db_connection, max_share_expiration_time_secs);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
2 changes: 2 additions & 0 deletions crates/server/src/model/share.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

Expand All @@ -6,4 +7,5 @@ pub struct Share {
pub id: Uuid,
pub json: String,
pub query: String,
pub expires_at: DateTime<Utc>,
}
55 changes: 45 additions & 10 deletions crates/server/src/routes/shares.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,78 @@ use axum::{
use serde::Deserialize;
use uuid::Uuid;

use crate::{model::share::Share, services::share::ShareService, AppState};
use crate::{
dto::error_object::ErrorObject,
services::share::{CreateShareError, ShareService},
AppState,
};

pub const SHARES_CONTEXT: &str = "/shares";

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct CreateShareRequest {
json: String,
query: String,
expiration_time_secs: i64,
}

impl From<&CreateShareError> for ErrorObject {
fn from(error: &CreateShareError) -> Self {
match error {
CreateShareError::InvalidExpirationTime { .. } => {
ErrorObject::new(error.to_string(), StatusCode::BAD_REQUEST)
}
CreateShareError::DatabaseError(_) => ErrorObject::new(
"Database error".to_string(),
StatusCode::INTERNAL_SERVER_ERROR,
),
}
}
}

async fn create_share(
State(shares_service): State<ShareService>,
Json(request): Json<CreateShareRequest>,
) -> impl IntoResponse {
let share_id = shares_service
.create_share(request.json, request.query)
.await
// TODO: handle unwrap. Log the error and return a 500 without giving away too much info
// we should return a Error Object with a trace id to trace the error in the logs
.unwrap();
let create_result = shares_service
.create_share(request.json, request.query, request.expiration_time_secs)
.await;

let share_id = match create_result {
Ok(share_id) => share_id,
Err(error) => {
let error_object = ErrorObject::from(&error);
// TODO: improve this
tracing::error!(
"Returning error response with trace id: {}. Original error: {error}",
error_object.trace_id
);
return error_object.into_response();
}
};

let mut headers = HeaderMap::new();
headers.insert(
"Location",
format!("{SHARES_CONTEXT}/{share_id}").parse().unwrap(),
);

(StatusCode::CREATED, headers)
(StatusCode::CREATED, headers).into_response()
}

async fn get_share(
State(shares_service): State<ShareService>,
Path(id): Path<Uuid>,
) -> Json<Share> {
) -> impl IntoResponse {
// TODO: handle unwrap. Log the error and return a 500 without giving away too much info
// we should return a Error Object with a trace id to trace the error in the logs
let share = shares_service.get_share(id).await.unwrap();
Json(share)
// TODO: create a ShareDTO and return it
match share {
Some(share) => Json(share).into_response(),
None => (StatusCode::NOT_FOUND).into_response(),
}
}

pub fn router() -> Router<AppState> {
Expand Down
Loading

0 comments on commit d3e660f

Please sign in to comment.