SQLX
This commit is contained in:
6
.env.example
Normal file
6
.env.example
Normal 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
2
.gitignore
vendored
@@ -66,3 +66,5 @@ coverage/
|
|||||||
# Storybook build outputs
|
# Storybook build outputs
|
||||||
.out
|
.out
|
||||||
.storybook-out
|
.storybook-out
|
||||||
|
|
||||||
|
.env
|
||||||
@@ -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"
|
||||||
|
|||||||
58
backend/migrations/001_initial.sql
Normal file
58
backend/migrations/001_initial.sql
Normal 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();
|
||||||
@@ -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?;
|
||||||
|
|||||||
8
backend/src/models/api_response.rs
Normal file
8
backend/src/models/api_response.rs
Normal 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>,
|
||||||
|
}
|
||||||
@@ -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>,
|
|
||||||
}
|
|
||||||
|
|||||||
24
backend/src/models/project.rs
Normal file
24
backend/src/models/project.rs
Normal 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>,
|
||||||
|
}
|
||||||
38
backend/src/models/task.rs
Normal file
38
backend/src/models/task.rs
Normal 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>,
|
||||||
|
}
|
||||||
25
backend/src/models/user.rs
Normal file
25
backend/src/models/user.rs
Normal 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>,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user