fix(rust): match main's drizzle-style camelCase response shape
Frontend reads main's drizzle-generated camelCase fields (siteId,
accessToken, oauthProvider, etc.) and a nested `site: {...}` on each
account row. Rust models were serializing snake_case with no nested
site, so PUT /api/sites/undefined/disabled-models 404s, account.site?.id
is undefined everywhere, and similar shape mismatches break Accounts,
Sites, Tokens, and Dashboard pages.
- Add #[serde(rename_all = "camelCase")] to every model struct in
src/db/models/* (Account, AccountToken, Site, User, Event,
TokenRoute, RouteChannel, BackgroundTask, DownstreamApiKey,
Organization/Member/Invitation, SiteAnnouncement)
- list_accounts now nests `site: {...}` per row, matching main's
`{...account, site: {...}}` shape from accountsOverviewService
- admin user-detail endpoint emits camelCase keys (totalRequests,
totalCost, last7DaysCost, recentLogs, promptTokens, completionTokens,
totalTokens, createdAt) to match the frontend
- routes refresh endpoint now emits both affected_routes and
affectedRoutes for forward compat
Co-Authored-By: Kiro <noreply@kiro.dev>
This commit is contained in:
@@ -3,6 +3,7 @@ use sqlx::FromRow;
|
|||||||
|
|
||||||
/// Represents an account bound to a site.
|
/// Represents an account bound to a site.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Account {
|
pub struct Account {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub site_id: i64,
|
pub site_id: i64,
|
||||||
@@ -30,6 +31,7 @@ pub struct Account {
|
|||||||
|
|
||||||
/// Represents an account token (from account_tokens table).
|
/// Represents an account token (from account_tokens table).
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AccountToken {
|
pub struct AccountToken {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub account_id: i64,
|
pub account_id: i64,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use sqlx::FromRow;
|
|||||||
|
|
||||||
/// Background task for tracking async operations (deployments, syncs, etc.)
|
/// Background task for tracking async operations (deployments, syncs, etc.)
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct BackgroundTask {
|
pub struct BackgroundTask {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub task_type: String,
|
pub task_type: String,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use sqlx::FromRow;
|
|||||||
|
|
||||||
/// Represents a downstream API key for external consumers.
|
/// Represents a downstream API key for external consumers.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct DownstreamApiKey {
|
pub struct DownstreamApiKey {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use sqlx::FromRow;
|
|||||||
|
|
||||||
/// System event for admin notifications.
|
/// System event for admin notifications.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Event {
|
pub struct Event {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
#[sqlx(rename = "type")]
|
#[sqlx(rename = "type")]
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use sqlx::FromRow;
|
|||||||
|
|
||||||
/// Organization for multi-tenant setup.
|
/// Organization for multi-tenant setup.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Organization {
|
pub struct Organization {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -15,6 +16,7 @@ pub struct Organization {
|
|||||||
|
|
||||||
/// Member of an organization.
|
/// Member of an organization.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct OrganizationMember {
|
pub struct OrganizationMember {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub org_id: i64,
|
pub org_id: i64,
|
||||||
@@ -26,6 +28,7 @@ pub struct OrganizationMember {
|
|||||||
|
|
||||||
/// Invitation to join an organization.
|
/// Invitation to join an organization.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Invitation {
|
pub struct Invitation {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub org_id: i64,
|
pub org_id: i64,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use sqlx::FromRow;
|
|||||||
|
|
||||||
/// Represents a route channel linking routes, accounts, and tokens.
|
/// Represents a route channel linking routes, accounts, and tokens.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct RouteChannel {
|
pub struct RouteChannel {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub route_id: i64,
|
pub route_id: i64,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use sqlx::FromRow;
|
|||||||
|
|
||||||
/// Represents a site (AI provider endpoint).
|
/// Represents a site (AI provider endpoint).
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Site {
|
pub struct Site {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use sqlx::FromRow;
|
|||||||
|
|
||||||
/// Site-level announcements (notifications from upstream providers).
|
/// Site-level announcements (notifications from upstream providers).
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SiteAnnouncement {
|
pub struct SiteAnnouncement {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub site_id: i64,
|
pub site_id: i64,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use sqlx::FromRow;
|
|||||||
|
|
||||||
/// Represents a token route (model routing rule).
|
/// Represents a token route (model routing rule).
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct TokenRoute {
|
pub struct TokenRoute {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub model_pattern: String,
|
pub model_pattern: String,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use sqlx::FromRow;
|
|||||||
|
|
||||||
/// A platform user (admin or regular).
|
/// A platform user (admin or regular).
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
|||||||
@@ -109,9 +109,31 @@ pub async fn list_accounts(
|
|||||||
.fetch_all(&*state.db)
|
.fetch_all(&*state.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// Nest the site object on each account so the frontend (which expects
|
||||||
|
// main's drizzle-style `{...account, site: {...}}` shape) can read
|
||||||
|
// `account.site.id`, `account.site.name`, `account.site.status`.
|
||||||
|
let site_by_id: std::collections::HashMap<i64, &Site> =
|
||||||
|
sites.iter().map(|s| (s.id, s)).collect();
|
||||||
|
|
||||||
|
let enriched: Vec<Value> = accounts
|
||||||
|
.iter()
|
||||||
|
.map(|account| {
|
||||||
|
let mut row = serde_json::to_value(account).unwrap_or(json!({}));
|
||||||
|
if let Some(site) = site_by_id.get(&account.site_id) {
|
||||||
|
if let Value::Object(map) = &mut row {
|
||||||
|
map.insert(
|
||||||
|
"site".to_string(),
|
||||||
|
serde_json::to_value(site).unwrap_or(Value::Null),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"data": accounts,
|
"data": enriched,
|
||||||
"accounts": accounts,
|
"accounts": enriched,
|
||||||
"sites": sites,
|
"sites": sites,
|
||||||
"generatedAt": chrono::Utc::now().to_rfc3339(),
|
"generatedAt": chrono::Utc::now().to_rfc3339(),
|
||||||
})))
|
})))
|
||||||
|
|||||||
@@ -236,7 +236,8 @@ pub async fn refresh_route(
|
|||||||
return Ok(Json(json!({
|
return Ok(Json(json!({
|
||||||
"data": {
|
"data": {
|
||||||
"refreshed": true,
|
"refreshed": true,
|
||||||
"affected_routes": result.rows_affected()
|
"affected_routes": result.rows_affected(),
|
||||||
|
"affectedRoutes": result.rows_affected()
|
||||||
}
|
}
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
@@ -251,7 +252,8 @@ pub async fn refresh_route(
|
|||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"data": {
|
"data": {
|
||||||
"refreshed": true,
|
"refreshed": true,
|
||||||
"affected_routes": result.rows_affected()
|
"affected_routes": result.rows_affected(),
|
||||||
|
"affectedRoutes": result.rows_affected()
|
||||||
}
|
}
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -341,22 +341,22 @@ pub async fn get_user_usage(
|
|||||||
json!({
|
json!({
|
||||||
"id": row.get::<i64, _>("id"),
|
"id": row.get::<i64, _>("id"),
|
||||||
"model": row.get::<Option<String>, _>("model"),
|
"model": row.get::<Option<String>, _>("model"),
|
||||||
"prompt_tokens": row.get::<Option<i64>, _>("prompt_tokens").unwrap_or(0),
|
"promptTokens": row.get::<Option<i64>, _>("prompt_tokens").unwrap_or(0),
|
||||||
"completion_tokens": row.get::<Option<i64>, _>("completion_tokens").unwrap_or(0),
|
"completionTokens": row.get::<Option<i64>, _>("completion_tokens").unwrap_or(0),
|
||||||
"total_tokens": row.get::<Option<i64>, _>("total_tokens").unwrap_or(0),
|
"totalTokens": row.get::<Option<i64>, _>("total_tokens").unwrap_or(0),
|
||||||
"cost": row.get::<Option<f64>, _>("cost").unwrap_or(0.0),
|
"cost": row.get::<Option<f64>, _>("cost").unwrap_or(0.0),
|
||||||
"created_at": row.get::<Option<String>, _>("created_at"),
|
"createdAt": row.get::<Option<String>, _>("created_at"),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"data": {
|
"data": {
|
||||||
"total_requests": stats.0,
|
"totalRequests": stats.0,
|
||||||
"total_cost": stats.1,
|
"totalCost": stats.1,
|
||||||
"last_7_days_cost": last_7_days_cost,
|
"last7DaysCost": last_7_days_cost,
|
||||||
"trend": trend.into_iter().map(|t| json!({ "day": t.0, "requests": t.1, "cost": t.2 })).collect::<Vec<_>>(),
|
"trend": trend.into_iter().map(|t| json!({ "day": t.0, "requests": t.1, "cost": t.2 })).collect::<Vec<_>>(),
|
||||||
"recent_logs": recent_logs,
|
"recentLogs": recent_logs,
|
||||||
}
|
}
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user