Use autogen types
This commit is contained in:
@@ -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
4
backend/build.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
fn main() {
|
||||
// Tell cargo to rerun build script if models change
|
||||
println!("cargo:rerun-if-changed=src/models/");
|
||||
}
|
||||
34
backend/src/bin/generate_types.rs
Normal file
34
backend/src/bin/generate_types.rs
Normal 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
3
backend/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod auth;
|
||||
pub mod models;
|
||||
pub mod routes;
|
||||
@@ -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())
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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, };
|
||||
|
||||
Reference in New Issue
Block a user