Use autogen types
This commit is contained in:
@@ -2,6 +2,11 @@
|
|||||||
name = "bloop-backend"
|
name = "bloop-backend"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
default-run = "bloop-backend"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "bloop_backend"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
@@ -19,3 +24,7 @@ uuid = { version = "1.0", features = ["v4", "serde"] }
|
|||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
bcrypt = "0.15"
|
bcrypt = "0.15"
|
||||||
jsonwebtoken = "9.2"
|
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::{
|
use axum::{
|
||||||
extract::{Extension, Query},
|
extract::Extension,
|
||||||
response::Json as ResponseJson,
|
response::Json as ResponseJson,
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use sqlx::postgres::PgPoolOptions;
|
||||||
use sqlx::{postgres::PgPoolOptions, PgPool};
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use tower_http::cors::CorsLayer;
|
use tower_http::cors::CorsLayer;
|
||||||
use tracing_subscriber;
|
use tracing_subscriber;
|
||||||
@@ -18,22 +17,7 @@ use auth::hash_password;
|
|||||||
use models::ApiResponse;
|
use models::ApiResponse;
|
||||||
use routes::{health, projects, users};
|
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(
|
async fn echo_handler(
|
||||||
Json(payload): Json<serde_json::Value>,
|
Json(payload): Json<serde_json::Value>,
|
||||||
@@ -71,7 +55,6 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
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("/echo", post(echo_handler))
|
.route("/echo", post(echo_handler))
|
||||||
.merge(projects::projects_router())
|
.merge(projects::projects_router())
|
||||||
.merge(users::users_router())
|
.merge(users::users_router())
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize, TS)]
|
||||||
|
#[ts(export)]
|
||||||
pub struct ApiResponse<T> {
|
pub struct ApiResponse<T> {
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
pub data: Option<T>,
|
pub data: Option<T>,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ pub mod project;
|
|||||||
pub mod task;
|
pub mod task;
|
||||||
pub mod api_response;
|
pub mod api_response;
|
||||||
|
|
||||||
pub use user::User;
|
pub use user::{User, CreateUser, UpdateUser, LoginRequest, LoginResponse, UserResponse};
|
||||||
pub use project::Project;
|
pub use project::{Project, CreateProject, UpdateProject};
|
||||||
pub use task::{Task, TaskStatus};
|
pub use task::{Task, TaskStatus};
|
||||||
pub use api_response::ApiResponse;
|
pub use api_response::ApiResponse;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::FromRow;
|
use sqlx::FromRow;
|
||||||
|
use ts_rs::TS;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
#[derive(Debug, Clone, FromRow, Serialize, Deserialize, TS)]
|
||||||
|
#[ts(export)]
|
||||||
pub struct Project {
|
pub struct Project {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -12,12 +14,14 @@ pub struct Project {
|
|||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, TS)]
|
||||||
|
#[ts(export)]
|
||||||
pub struct CreateProject {
|
pub struct CreateProject {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, TS)]
|
||||||
|
#[ts(export)]
|
||||||
pub struct UpdateProject {
|
pub struct UpdateProject {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::FromRow;
|
use sqlx::FromRow;
|
||||||
|
use ts_rs::TS;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
||||||
@@ -14,33 +15,39 @@ pub struct User {
|
|||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, TS)]
|
||||||
|
#[ts(export)]
|
||||||
pub struct CreateUser {
|
pub struct CreateUser {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
pub is_admin: Option<bool>,
|
pub is_admin: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, TS)]
|
||||||
|
#[ts(export)]
|
||||||
pub struct UpdateUser {
|
pub struct UpdateUser {
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
pub is_admin: Option<bool>,
|
pub is_admin: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, TS)]
|
||||||
|
#[ts(export)]
|
||||||
pub struct LoginRequest {
|
pub struct LoginRequest {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize, TS)]
|
||||||
|
#[ts(export)]
|
||||||
pub struct LoginResponse {
|
pub struct LoginResponse {
|
||||||
pub user: UserResponse,
|
pub user: UserResponse,
|
||||||
pub token: String,
|
pub token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize, TS)]
|
||||||
|
#[ts(export)]
|
||||||
|
#[ts(rename = "User")]
|
||||||
pub struct UserResponse {
|
pub struct UserResponse {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
|
|||||||
@@ -34,14 +34,14 @@ export function UserForm({ open, onClose, onSuccess, user }: UserFormProps) {
|
|||||||
try {
|
try {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
const updateData: UpdateUser = {
|
const updateData: UpdateUser = {
|
||||||
email: email !== user.email ? email : undefined,
|
email: email !== user.email ? email : null,
|
||||||
password: password ? password : undefined,
|
password: password ? password : null,
|
||||||
is_admin: canEditAdminStatus && isAdmin !== user.is_admin ? isAdmin : undefined
|
is_admin: canEditAdminStatus && isAdmin !== user.is_admin ? isAdmin : null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove undefined values
|
// Remove null values
|
||||||
Object.keys(updateData).forEach(key => {
|
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]
|
delete updateData[key as keyof UpdateUser]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,20 +16,7 @@ export function HomePage() {
|
|||||||
|
|
||||||
const currentUser = authStorage.getUser()
|
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 () => {
|
const checkHealth = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -89,33 +76,7 @@ export function HomePage() {
|
|||||||
|
|
||||||
{/* Feature Cards */}
|
{/* Feature Cards */}
|
||||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3 mb-8">
|
<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">
|
<Card className="group hover:shadow-lg transition-all duration-200 border-muted/50 hover:border-muted">
|
||||||
<CardHeader className="pb-4">
|
<CardHeader className="pb-4">
|
||||||
@@ -172,7 +133,7 @@ export function HomePage() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{currentUser?.is_admin && (
|
{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">
|
<CardHeader className="pb-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"frontend:build": "cd frontend && npm run build",
|
"frontend:build": "cd frontend && npm run build",
|
||||||
"backend:dev": "cargo watch -x 'run --manifest-path backend/Cargo.toml'",
|
"backend:dev": "cargo watch -x 'run --manifest-path backend/Cargo.toml'",
|
||||||
"backend:build": "cargo build --release --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": {
|
"devDependencies": {
|
||||||
"concurrently": "^8.2.2"
|
"concurrently": "^8.2.2"
|
||||||
|
|||||||
@@ -1,60 +1,20 @@
|
|||||||
// Shared types between frontend and backend
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
export interface ApiResponse<T> {
|
// Auto-generated from Rust backend types using ts-rs
|
||||||
success: boolean
|
|
||||||
data?: T
|
|
||||||
message?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HelloResponse {
|
export type ApiResponse<T> = { success: boolean, data: T | null, message: string | null, };
|
||||||
message: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HelloQuery {
|
export type CreateProject = { name: string, };
|
||||||
name?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Project {
|
export type CreateUser = { email: string, password: string, is_admin: boolean | null, };
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
owner_id: string
|
|
||||||
created_at: string
|
|
||||||
updated_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateProject {
|
export type LoginRequest = { email: string, password: string, };
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateProject {
|
export type LoginResponse = { user: User, token: string, };
|
||||||
name?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface User {
|
export type Project = { id: string, name: string, owner_id: string, created_at: string, updated_at: string, };
|
||||||
id: string
|
|
||||||
email: string
|
|
||||||
is_admin: boolean
|
|
||||||
created_at: string
|
|
||||||
updated_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateUser {
|
export type UpdateProject = { name: string | null, };
|
||||||
email: string
|
|
||||||
password: string
|
|
||||||
is_admin?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateUser {
|
export type UpdateUser = { email: string | null, password: string | null, is_admin: boolean | null, };
|
||||||
email?: string
|
|
||||||
password?: string
|
|
||||||
is_admin?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LoginRequest {
|
export type User = { id: string, email: string, is_admin: boolean, created_at: string, updated_at: string, };
|
||||||
email: string
|
|
||||||
password: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LoginResponse {
|
|
||||||
user: User
|
|
||||||
token: string
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user