Skip to main content

postrust_server/
custom.rs

1//! Custom routes extension for Postrust.
2//!
3//! Add your custom API endpoints here that don't go through the
4//! standard PostgREST-style query processing.
5
6use axum::{
7    extract::State,
8    http::StatusCode,
9    response::{IntoResponse, Json},
10    routing::get,
11    Router,
12};
13use serde::{Deserialize, Serialize};
14use std::sync::Arc;
15
16use crate::state::AppState;
17
18/// Build the custom routes router.
19pub fn custom_router() -> Router<Arc<AppState>> {
20    Router::new()
21        .route("/health", get(health_check))
22        .route("/ready", get(readiness_check))
23    // Add your custom routes here:
24    // .route("/webhooks/stripe", post(handle_stripe_webhook))
25    // .route("/email/send", post(send_email))
26}
27
28/// Build the router served on `admin_server_port`.
29///
30/// Liveness and readiness only. The point of a separate port is that it can be
31/// reachable from an orchestrator without the API being reachable from the same
32/// place, so nothing that reads or writes data belongs here.
33///
34/// `/live` and `/ready` are the names PostgREST's admin server uses; `/health`
35/// is the same answer as `/live` under the name the main API already serves.
36pub fn admin_router() -> Router<Arc<AppState>> {
37    Router::new()
38        .route("/live", get(health_check))
39        .route("/health", get(health_check))
40        .route("/ready", get(readiness_check))
41}
42
43// =============================================================================
44// Health & Readiness
45// =============================================================================
46
47async fn health_check() -> Json<HealthResponse> {
48    Json(HealthResponse {
49        status: "ok".to_string(),
50        version: env!("CARGO_PKG_VERSION").to_string(),
51    })
52}
53
54async fn readiness_check(State(state): State<Arc<AppState>>) -> impl IntoResponse {
55    // Check database connectivity
56    match sqlx::query("SELECT 1").execute(&state.pool).await {
57        Ok(_) => (
58            StatusCode::OK,
59            Json(ReadinessResponse {
60                ready: true,
61                database: true,
62                message: None,
63            }),
64        ),
65        Err(e) => (
66            StatusCode::SERVICE_UNAVAILABLE,
67            Json(ReadinessResponse {
68                ready: false,
69                database: false,
70                message: Some(e.to_string()),
71            }),
72        ),
73    }
74}
75
76// =============================================================================
77// Response Types
78// =============================================================================
79
80#[derive(Serialize)]
81struct HealthResponse {
82    status: String,
83    version: String,
84}
85
86#[derive(Serialize)]
87struct ReadinessResponse {
88    ready: bool,
89    database: bool,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    message: Option<String>,
92}
93
94// =============================================================================
95// Example: Webhook Handler
96// =============================================================================
97
98#[derive(Deserialize)]
99#[allow(dead_code)]
100struct WebhookPayload {
101    event_type: String,
102    data: serde_json::Value,
103}
104
105#[allow(dead_code)]
106async fn handle_webhook(
107    State(state): State<Arc<AppState>>,
108    Json(payload): Json<WebhookPayload>,
109) -> impl IntoResponse {
110    tracing::info!("Received webhook: {}", payload.event_type);
111
112    // Process webhook with database access
113    let _pool = &state.pool;
114
115    // Your custom logic here...
116
117    (
118        StatusCode::OK,
119        Json(serde_json::json!({ "received": true })),
120    )
121}
122
123// =============================================================================
124// Example: Custom RPC that bypasses PostgREST
125// =============================================================================
126
127#[derive(Deserialize)]
128#[allow(dead_code)]
129struct CustomRpcRequest {
130    action: String,
131    params: serde_json::Value,
132}
133
134#[allow(dead_code)]
135async fn custom_rpc(
136    State(state): State<Arc<AppState>>,
137    Json(req): Json<CustomRpcRequest>,
138) -> impl IntoResponse {
139    // Direct database access for custom operations
140    let result = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM users")
141        .fetch_one(&state.pool)
142        .await;
143
144    match result {
145        Ok(count) => (
146            StatusCode::OK,
147            Json(serde_json::json!({
148                "action": req.action,
149                "result": count
150            })),
151        ),
152        Err(e) => (
153            StatusCode::INTERNAL_SERVER_ERROR,
154            Json(serde_json::json!({
155                "error": e.to_string()
156            })),
157        ),
158    }
159}