Use autogen types

This commit is contained in:
Louis Knight-Webb
2025-06-14 17:36:54 -04:00
parent a7ef8604d1
commit 5dbfc648fe
13 changed files with 97 additions and 129 deletions

View File

@@ -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"] }

4
backend/build.rs Normal file
View File

@@ -0,0 +1,4 @@
fn main() {
// Tell cargo to rerun build script if models change
println!("cargo:rerun-if-changed=src/models/");
}

View File

@@ -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/");
}

3
backend/src/lib.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod auth;
pub mod models;
pub mod routes;

View File

@@ -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<String>,
}
#[derive(Debug, Serialize)]
struct HelloResponse {
message: String,
}
async fn hello_handler(Query(params): Query<HelloQuery>) -> ResponseJson<HelloResponse> {
let name = params.name.unwrap_or_else(|| "World".to_string());
ResponseJson(HelloResponse {
message: format!("Hello, {}!", name),
})
}
async fn echo_handler(
Json(payload): Json<serde_json::Value>,
@@ -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())

View File

@@ -1,6 +1,8 @@
use serde::Serialize;
use ts_rs::TS;
#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, TS)]
#[ts(export)]
pub struct ApiResponse<T> {
pub success: bool,
pub data: Option<T>,

View File

@@ -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;

View File

@@ -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<Utc>,
}
#[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<String>,
}

View File

@@ -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<Utc>,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, TS)]
#[ts(export)]
pub struct CreateUser {
pub email: String,
pub password: String,
pub is_admin: Option<bool>,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, TS)]
#[ts(export)]
pub struct UpdateUser {
pub email: Option<String>,
pub password: Option<String>,
pub is_admin: Option<bool>,
}
#[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,

View File

@@ -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]
}
})

View File

@@ -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 */}
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3 mb-8">
<Card className="group hover:shadow-lg transition-all duration-200 border-muted/50 hover:border-muted">
<CardHeader className="pb-4">
<div className="flex items-center justify-between">
<div className="flex items-center">
<div className="rounded-lg bg-primary/10 p-2 mr-3 group-hover:bg-primary/20 transition-colors">
<Heart className="h-5 w-5 text-primary" />
</div>
<CardTitle className="text-lg">API Test</CardTitle>
</div>
<Badge variant="secondary" className="text-xs">Test</Badge>
</div>
<CardDescription>
Test the connection between frontend and backend
</CardDescription>
</CardHeader>
<CardContent>
<Button
onClick={fetchHello}
disabled={loading}
className="w-full group-hover:shadow-sm transition-shadow"
size="sm"
>
<Heart className="mr-2 h-4 w-4" />
{loading ? 'Testing...' : 'Say Hello'}
</Button>
</CardContent>
</Card>
<Card className="group hover:shadow-lg transition-all duration-200 border-muted/50 hover:border-muted">
<CardHeader className="pb-4">
@@ -172,7 +133,7 @@ export function HomePage() {
</Card>
{currentUser?.is_admin && (
<Card className="group hover:shadow-lg transition-all duration-200 border-muted/50 hover:border-muted lg:col-span-2">
<Card className="group hover:shadow-lg transition-all duration-200 border-muted/50 hover:border-muted">
<CardHeader className="pb-4">
<div className="flex items-center justify-between">
<div className="flex items-center">

View File

@@ -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"

View File

@@ -1,60 +1,20 @@
// Shared types between frontend and backend
export interface ApiResponse<T> {
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<T> = { 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, };