From 5dbfc648fe45d62c0e272a411186775b5b5fbd0f Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Sat, 14 Jun 2025 17:36:54 -0400 Subject: [PATCH] Use autogen types --- backend/Cargo.toml | 9 +++ backend/build.rs | 4 ++ backend/src/bin/generate_types.rs | 34 +++++++++++ backend/src/lib.rs | 3 + backend/src/main.rs | 21 +------ backend/src/models/api_response.rs | 4 +- backend/src/models/mod.rs | 4 +- backend/src/models/project.rs | 10 +++- backend/src/models/user.rs | 17 ++++-- frontend/src/components/users/user-form.tsx | 10 ++-- frontend/src/pages/home.tsx | 45 +-------------- package.json | 3 +- shared/types.ts | 62 ++++----------------- 13 files changed, 97 insertions(+), 129 deletions(-) create mode 100644 backend/build.rs create mode 100644 backend/src/bin/generate_types.rs create mode 100644 backend/src/lib.rs diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 23cff90c..1dffbf8d 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -2,6 +2,11 @@ name = "bloop-backend" version = "0.1.0" edition = "2021" +default-run = "bloop-backend" + +[lib] +name = "bloop_backend" +path = "src/lib.rs" [dependencies] tokio = { workspace = true } @@ -19,3 +24,7 @@ uuid = { version = "1.0", features = ["v4", "serde"] } dotenvy = "0.15" bcrypt = "0.15" jsonwebtoken = "9.2" +ts-rs = { version = "9.0", features = ["uuid-impl", "chrono-impl"] } + +[build-dependencies] +ts-rs = { version = "9.0", features = ["uuid-impl", "chrono-impl"] } diff --git a/backend/build.rs b/backend/build.rs new file mode 100644 index 00000000..87210b10 --- /dev/null +++ b/backend/build.rs @@ -0,0 +1,4 @@ +fn main() { + // Tell cargo to rerun build script if models change + println!("cargo:rerun-if-changed=src/models/"); +} diff --git a/backend/src/bin/generate_types.rs b/backend/src/bin/generate_types.rs new file mode 100644 index 00000000..584b60da --- /dev/null +++ b/backend/src/bin/generate_types.rs @@ -0,0 +1,34 @@ +use std::env; +use std::path::Path; +use ts_rs::TS; + +// Import all the types we want to export using the library crate +use bloop_backend::models::{ + ApiResponse, Project, CreateProject, UpdateProject, + CreateUser, UpdateUser, LoginRequest, LoginResponse, UserResponse, +}; + +fn main() { + let shared_path = Path::new("../shared"); + + // Create the shared directory if it doesn't exist + std::fs::create_dir_all(shared_path).unwrap(); + + println!("Generating TypeScript types..."); + + // Set environment variable to configure ts-rs output directory + env::set_var("TS_RS_EXPORT_DIR", shared_path.to_str().unwrap()); + + // Export TypeScript types for each struct using ts-rs export functionality + bloop_backend::models::ApiResponse::<()>::export().unwrap(); + bloop_backend::models::Project::export().unwrap(); + bloop_backend::models::CreateProject::export().unwrap(); + bloop_backend::models::UpdateProject::export().unwrap(); + bloop_backend::models::CreateUser::export().unwrap(); + bloop_backend::models::UpdateUser::export().unwrap(); + bloop_backend::models::LoginRequest::export().unwrap(); + bloop_backend::models::LoginResponse::export().unwrap(); + bloop_backend::models::UserResponse::export().unwrap(); + + println!("TypeScript types generated successfully in ../shared/"); +} diff --git a/backend/src/lib.rs b/backend/src/lib.rs new file mode 100644 index 00000000..cdcd9d85 --- /dev/null +++ b/backend/src/lib.rs @@ -0,0 +1,3 @@ +pub mod auth; +pub mod models; +pub mod routes; diff --git a/backend/src/main.rs b/backend/src/main.rs index 3f280ae6..49565727 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,11 +1,10 @@ use axum::{ - extract::{Extension, Query}, + extract::Extension, response::Json as ResponseJson, routing::{get, post}, Json, Router, }; -use serde::{Deserialize, Serialize}; -use sqlx::{postgres::PgPoolOptions, PgPool}; +use sqlx::postgres::PgPoolOptions; use std::env; use tower_http::cors::CorsLayer; use tracing_subscriber; @@ -18,22 +17,7 @@ use auth::hash_password; use models::ApiResponse; use routes::{health, projects, users}; -#[derive(Debug, Deserialize)] -struct HelloQuery { - name: Option, -} -#[derive(Debug, Serialize)] -struct HelloResponse { - message: String, -} - -async fn hello_handler(Query(params): Query) -> ResponseJson { - let name = params.name.unwrap_or_else(|| "World".to_string()); - ResponseJson(HelloResponse { - message: format!("Hello, {}!", name), - }) -} async fn echo_handler( Json(payload): Json, @@ -71,7 +55,6 @@ async fn main() -> anyhow::Result<()> { let app = Router::new() .route("/", get(|| async { "Bloop API" })) .route("/health", get(health::health_check)) - .route("/hello", get(hello_handler)) .route("/echo", post(echo_handler)) .merge(projects::projects_router()) .merge(users::users_router()) diff --git a/backend/src/models/api_response.rs b/backend/src/models/api_response.rs index 355efa17..aa89cf0d 100644 --- a/backend/src/models/api_response.rs +++ b/backend/src/models/api_response.rs @@ -1,6 +1,8 @@ use serde::Serialize; +use ts_rs::TS; -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, TS)] +#[ts(export)] pub struct ApiResponse { pub success: bool, pub data: Option, diff --git a/backend/src/models/mod.rs b/backend/src/models/mod.rs index 99e8a8e4..c990fdb2 100644 --- a/backend/src/models/mod.rs +++ b/backend/src/models/mod.rs @@ -3,7 +3,7 @@ pub mod project; pub mod task; pub mod api_response; -pub use user::User; -pub use project::Project; +pub use user::{User, CreateUser, UpdateUser, LoginRequest, LoginResponse, UserResponse}; +pub use project::{Project, CreateProject, UpdateProject}; pub use task::{Task, TaskStatus}; pub use api_response::ApiResponse; diff --git a/backend/src/models/project.rs b/backend/src/models/project.rs index 05516cdb..e6bbf7e0 100644 --- a/backend/src/models/project.rs +++ b/backend/src/models/project.rs @@ -1,9 +1,11 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::FromRow; +use ts_rs::TS; use uuid::Uuid; -#[derive(Debug, Clone, FromRow, Serialize, Deserialize)] +#[derive(Debug, Clone, FromRow, Serialize, Deserialize, TS)] +#[ts(export)] pub struct Project { pub id: Uuid, pub name: String, @@ -12,12 +14,14 @@ pub struct Project { pub updated_at: DateTime, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, TS)] +#[ts(export)] pub struct CreateProject { pub name: String, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, TS)] +#[ts(export)] pub struct UpdateProject { pub name: Option, } diff --git a/backend/src/models/user.rs b/backend/src/models/user.rs index a5117729..d70268fa 100644 --- a/backend/src/models/user.rs +++ b/backend/src/models/user.rs @@ -1,6 +1,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::FromRow; +use ts_rs::TS; use uuid::Uuid; #[derive(Debug, Clone, FromRow, Serialize, Deserialize)] @@ -14,33 +15,39 @@ pub struct User { pub updated_at: DateTime, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, TS)] +#[ts(export)] pub struct CreateUser { pub email: String, pub password: String, pub is_admin: Option, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, TS)] +#[ts(export)] pub struct UpdateUser { pub email: Option, pub password: Option, pub is_admin: Option, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, TS)] +#[ts(export)] pub struct LoginRequest { pub email: String, pub password: String, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, TS)] +#[ts(export)] pub struct LoginResponse { pub user: UserResponse, pub token: String, } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, TS)] +#[ts(export)] +#[ts(rename = "User")] pub struct UserResponse { pub id: Uuid, pub email: String, diff --git a/frontend/src/components/users/user-form.tsx b/frontend/src/components/users/user-form.tsx index bf5d6364..ff4dfa9f 100644 --- a/frontend/src/components/users/user-form.tsx +++ b/frontend/src/components/users/user-form.tsx @@ -34,14 +34,14 @@ export function UserForm({ open, onClose, onSuccess, user }: UserFormProps) { try { if (isEditing) { const updateData: UpdateUser = { - email: email !== user.email ? email : undefined, - password: password ? password : undefined, - is_admin: canEditAdminStatus && isAdmin !== user.is_admin ? isAdmin : undefined + email: email !== user.email ? email : null, + password: password ? password : null, + is_admin: canEditAdminStatus && isAdmin !== user.is_admin ? isAdmin : null } - // Remove undefined values + // Remove null values Object.keys(updateData).forEach(key => { - if (updateData[key as keyof UpdateUser] === undefined) { + if (updateData[key as keyof UpdateUser] === null) { delete updateData[key as keyof UpdateUser] } }) diff --git a/frontend/src/pages/home.tsx b/frontend/src/pages/home.tsx index e31f7f42..d2f7ce11 100644 --- a/frontend/src/pages/home.tsx +++ b/frontend/src/pages/home.tsx @@ -16,20 +16,7 @@ export function HomePage() { const currentUser = authStorage.getUser() - const fetchHello = async () => { - setLoading(true) - try { - const response = await makeAuthenticatedRequest('/api/hello?name=Bloop') - const data = await response.json() - setMessage(data.message) - setMessageType('success') - } catch (error) { - setMessage('Error connecting to backend') - setMessageType('error') - } finally { - setLoading(false) - } - } + const checkHealth = async () => { setLoading(true) @@ -89,33 +76,7 @@ export function HomePage() { {/* Feature Cards */}
- - -
-
-
- -
- API Test -
- Test -
- - Test the connection between frontend and backend - -
- - - -
+ @@ -172,7 +133,7 @@ export function HomePage() { {currentUser?.is_admin && ( - +
diff --git a/package.json b/package.json index f7370a44..47515755 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "frontend:build": "cd frontend && npm run build", "backend:dev": "cargo watch -x 'run --manifest-path backend/Cargo.toml'", "backend:build": "cargo build --release --manifest-path backend/Cargo.toml", - "backend:run": "cargo run --manifest-path backend/Cargo.toml" + "backend:run": "cargo run --manifest-path backend/Cargo.toml", + "generate-types": "cd backend && cargo run --bin generate_types" }, "devDependencies": { "concurrently": "^8.2.2" diff --git a/shared/types.ts b/shared/types.ts index 9f99ed30..b6c63313 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -1,60 +1,20 @@ -// Shared types between frontend and backend -export interface ApiResponse { - success: boolean - data?: T - message?: string -} +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +// Auto-generated from Rust backend types using ts-rs -export interface HelloResponse { - message: string -} +export type ApiResponse = { success: boolean, data: T | null, message: string | null, }; -export interface HelloQuery { - name?: string -} +export type CreateProject = { name: string, }; -export interface Project { - id: string - name: string - owner_id: string - created_at: string - updated_at: string -} +export type CreateUser = { email: string, password: string, is_admin: boolean | null, }; -export interface CreateProject { - name: string -} +export type LoginRequest = { email: string, password: string, }; -export interface UpdateProject { - name?: string -} +export type LoginResponse = { user: User, token: string, }; -export interface User { - id: string - email: string - is_admin: boolean - created_at: string - updated_at: string -} +export type Project = { id: string, name: string, owner_id: string, created_at: string, updated_at: string, }; -export interface CreateUser { - email: string - password: string - is_admin?: boolean -} +export type UpdateProject = { name: string | null, }; -export interface UpdateUser { - email?: string - password?: string - is_admin?: boolean -} +export type UpdateUser = { email: string | null, password: string | null, is_admin: boolean | null, }; -export interface LoginRequest { - email: string - password: string -} - -export interface LoginResponse { - user: User - token: string -} +export type User = { id: string, email: string, is_admin: boolean, created_at: string, updated_at: string, };