This commit is contained in:
Louis Knight-Webb
2025-06-14 15:34:24 -04:00
parent 563994934d
commit e099269ed2
10 changed files with 189 additions and 8 deletions

6
.env.example Normal file
View File

@@ -0,0 +1,6 @@
# Database Configuration
DATABASE_URL=postgresql://username:password@localhost:5432/mission_control
# Optional: Override default server settings
# SERVER_HOST=0.0.0.0
# SERVER_PORT=3001

2
.gitignore vendored
View File

@@ -66,3 +66,5 @@ coverage/
# Storybook build outputs # Storybook build outputs
.out .out
.storybook-out .storybook-out
.env

View File

@@ -13,3 +13,7 @@ serde_json = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tracing-subscriber = { workspace = true } tracing-subscriber = { workspace = true }
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "chrono", "uuid"] }
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1.0", features = ["v4", "serde"] }
dotenvy = "0.15"

View File

@@ -0,0 +1,58 @@
-- Create task_status enum
CREATE TYPE task_status AS ENUM ('todo', 'inprogress', 'done', 'cancelled');
-- Create users table
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Create projects table
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Create tasks table
CREATE TABLE tasks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
description TEXT,
status task_status NOT NULL DEFAULT 'todo',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Create indexes for better performance
CREATE INDEX idx_projects_owner_id ON projects(owner_id);
CREATE INDEX idx_tasks_project_id ON tasks(project_id);
CREATE INDEX idx_tasks_status ON tasks(status);
-- Create updated_at trigger function
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
-- Create triggers to auto-update updated_at
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_projects_updated_at
BEFORE UPDATE ON projects
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_tasks_updated_at
BEFORE UPDATE ON tasks
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

View File

@@ -3,11 +3,13 @@ use axum::{
Router, Router,
Json, Json,
response::Json as ResponseJson, response::Json as ResponseJson,
extract::Query, extract::{Query, Extension},
}; };
use tower_http::cors::CorsLayer; use tower_http::cors::CorsLayer;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing_subscriber; use tracing_subscriber;
use sqlx::{PgPool, postgres::PgPoolOptions};
use std::env;
mod routes; mod routes;
@@ -43,13 +45,26 @@ async fn echo_handler(Json(payload): Json<serde_json::Value>) -> ResponseJson<Ap
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
// Load environment variables from .env file
dotenvy::dotenv().ok();
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
// Database connection
let database_url = env::var("DATABASE_URL")
.expect("DATABASE_URL must be set in environment or .env file");
let pool = PgPoolOptions::new()
.max_connections(10)
.connect(&database_url)
.await?;
let app = Router::new() let app = Router::new()
.route("/", get(|| async { "Bloop API" })) .route("/", get(|| async { "Bloop API" }))
.route("/health", get(health::health_check)) .route("/health", get(health::health_check))
.route("/hello", get(hello_handler)) .route("/hello", get(hello_handler))
.route("/echo", post(echo_handler)) .route("/echo", post(echo_handler))
.layer(Extension(pool))
.layer(CorsLayer::permissive()); .layer(CorsLayer::permissive());
let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await?; let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await?;

View File

@@ -0,0 +1,8 @@
use serde::Serialize;
#[derive(Debug, Serialize)]
pub struct ApiResponse<T> {
pub success: bool,
pub data: Option<T>,
pub message: Option<String>,
}

View File

@@ -1,8 +1,9 @@
use serde::Serialize; pub mod user;
pub mod project;
pub mod task;
pub mod api_response;
#[derive(Debug, Serialize)] pub use user::User;
pub struct ApiResponse<T> { pub use project::Project;
pub success: bool, pub use task::{Task, TaskStatus};
pub data: Option<T>, pub use api_response::ApiResponse;
pub message: Option<String>,
}

View File

@@ -0,0 +1,24 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
pub struct Project {
pub id: Uuid,
pub name: String,
pub owner_id: Uuid, // Foreign key to User
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Deserialize)]
pub struct CreateProject {
pub name: String,
pub owner_id: Uuid,
}
#[derive(Debug, Deserialize)]
pub struct UpdateProject {
pub name: Option<String>,
}

View File

@@ -0,0 +1,38 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Type};
use uuid::Uuid;
#[derive(Debug, Clone, Type, Serialize, Deserialize, PartialEq)]
#[sqlx(type_name = "task_status", rename_all = "lowercase")]
pub enum TaskStatus {
Todo,
InProgress,
Done,
Cancelled,
}
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
pub struct Task {
pub id: Uuid,
pub project_id: Uuid, // Foreign key to Project
pub title: String,
pub description: Option<String>,
pub status: TaskStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Deserialize)]
pub struct CreateTask {
pub project_id: Uuid,
pub title: String,
pub description: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct UpdateTask {
pub title: Option<String>,
pub description: Option<String>,
pub status: Option<TaskStatus>,
}

View File

@@ -0,0 +1,25 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
pub struct User {
pub id: Uuid,
pub email: String,
pub password: String, // This should be hashed
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Deserialize)]
pub struct CreateUser {
pub email: String,
pub password: String,
}
#[derive(Debug, Deserialize)]
pub struct UpdateUser {
pub email: Option<String>,
pub password: Option<String>,
}