fraiseql_server/routes/studio/function_ops.rs
1//! Function operations endpoints for the Studio dashboard.
2//!
3//! Routes under `/admin/v1/functions/*` expose deployed function listing,
4//! invocation, log retrieval, and secrets management. All routes are
5//! protected by the admin bearer token middleware.
6
7use axum::{
8 Json,
9 extract::{Path, State},
10 http::StatusCode,
11 response::IntoResponse,
12};
13use fraiseql_core::db::traits::DatabaseAdapter;
14use serde::{Deserialize, Serialize};
15
16use crate::routes::graphql::app_state::AppState;
17
18// ---------------------------------------------------------------------------
19// Function record
20// ---------------------------------------------------------------------------
21
22/// A deployed function summary agreed with the Luxen UI author.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct FunctionEntry {
25 /// Function name / identifier.
26 pub name: String,
27 /// Deployment version number.
28 pub version: u32,
29 /// Runtime type (e.g. `"wasm"`, `"deno"`).
30 pub runtime: String,
31 /// Deployment status (`"active"`, `"inactive"`, `"error"`).
32 pub status: String,
33 /// Deployment timestamp (RFC 3339).
34 pub deployed_at: String,
35}
36
37// ---------------------------------------------------------------------------
38// Response types
39// ---------------------------------------------------------------------------
40
41/// Function list response agreed with the Luxen UI author.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct FunctionListResponse {
44 /// All deployed functions for this tenant.
45 pub functions: Vec<FunctionEntry>,
46}
47
48/// Function invocation result.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct InvokeResponse {
51 /// Return value from the function.
52 pub value: serde_json::Value,
53 /// Captured log lines from the invocation.
54 pub logs: Vec<String>,
55 /// Wall-clock duration of the invocation in milliseconds.
56 pub duration_ms: u64,
57}
58
59/// A single invocation log entry (ring-buffer record).
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct InvocationLogEntry {
62 /// Invocation outcome (`"ok"` or `"error"`).
63 pub status: String,
64 /// Duration of this invocation in milliseconds.
65 pub duration_ms: u64,
66 /// Error message, if `status == "error"`.
67 pub error: Option<String>,
68 /// Invocation timestamp (RFC 3339).
69 pub timestamp: String,
70}
71
72/// Secret keys list (values are never returned).
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct SecretsKeysResponse {
75 /// Secret key names for this function.
76 pub keys: Vec<String>,
77}
78
79// ---------------------------------------------------------------------------
80// Request types
81// ---------------------------------------------------------------------------
82
83/// Request body for `POST /admin/v1/functions/{name}/invoke`.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct InvokeRequest {
86 /// Event payload to pass to the function.
87 pub event: serde_json::Value,
88}
89
90/// Request body for `PUT /admin/v1/functions/{name}/secrets/{key}`.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct SecretSetRequest {
93 /// Secret value (encrypted and stored server-side).
94 pub value: String,
95}
96
97// ---------------------------------------------------------------------------
98// Handlers
99// ---------------------------------------------------------------------------
100
101/// `GET /admin/v1/functions` — list all deployed functions.
102///
103/// # Errors
104///
105/// Returns `401` without valid admin credentials (enforced by middleware).
106pub async fn list_functions_handler<A>(State(_state): State<AppState<A>>) -> impl IntoResponse
107where
108 A: DatabaseAdapter + Clone + Send + Sync + 'static,
109{
110 Json(FunctionListResponse { functions: vec![] })
111}
112
113/// `POST /admin/v1/functions/{name}/invoke` — invoke a function.
114///
115/// # Errors
116///
117/// Returns `401` without valid admin credentials (enforced by middleware).
118/// Returns `404` if the function does not exist.
119pub async fn invoke_function_handler<A>(
120 Path(_name): Path<String>,
121 State(_state): State<AppState<A>>,
122 Json(_req): Json<InvokeRequest>,
123) -> impl IntoResponse
124where
125 A: DatabaseAdapter + Clone + Send + Sync + 'static,
126{
127 (
128 StatusCode::NOT_IMPLEMENTED,
129 Json(serde_json::json!({
130 "error": "Not Implemented",
131 "message": "Function invocation endpoint available in a future release"
132 })),
133 )
134}
135
136/// `GET /admin/v1/functions/{name}/logs` — last N invocation log entries.
137///
138/// # Errors
139///
140/// Returns `401` without valid admin credentials (enforced by middleware).
141pub async fn function_logs_handler<A>(
142 Path(_name): Path<String>,
143 State(_state): State<AppState<A>>,
144) -> impl IntoResponse
145where
146 A: DatabaseAdapter + Clone + Send + Sync + 'static,
147{
148 Json(serde_json::json!({ "logs": [] }))
149}
150
151/// `GET /admin/v1/functions/{name}/secrets` — secret key names (values never returned).
152///
153/// # Errors
154///
155/// Returns `401` without valid admin credentials (enforced by middleware).
156pub async fn list_secrets_handler<A>(
157 Path(_name): Path<String>,
158 State(_state): State<AppState<A>>,
159) -> impl IntoResponse
160where
161 A: DatabaseAdapter + Clone + Send + Sync + 'static,
162{
163 Json(SecretsKeysResponse { keys: vec![] })
164}
165
166/// `PUT /admin/v1/functions/{name}/secrets/{key}` — set a secret value.
167///
168/// # Errors
169///
170/// Returns `401` without valid admin credentials (enforced by middleware).
171pub async fn set_secret_handler<A>(
172 Path((_name, _key)): Path<(String, String)>,
173 State(_state): State<AppState<A>>,
174 Json(_req): Json<SecretSetRequest>,
175) -> impl IntoResponse
176where
177 A: DatabaseAdapter + Clone + Send + Sync + 'static,
178{
179 Json(serde_json::json!({"success": true}))
180}
181
182/// `DELETE /admin/v1/functions/{name}/secrets/{key}` — delete a secret.
183///
184/// # Errors
185///
186/// Returns `401` without valid admin credentials (enforced by middleware).
187pub async fn delete_secret_handler<A>(
188 Path((_name, _key)): Path<(String, String)>,
189 State(_state): State<AppState<A>>,
190) -> impl IntoResponse
191where
192 A: DatabaseAdapter + Clone + Send + Sync + 'static,
193{
194 Json(serde_json::json!({"success": true}))
195}