Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add routes endpoint #314

Open
wants to merge 9 commits into
base: tauri-main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions fpx-cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ pub struct Args {
#[clap(long, env, default_value = "http://localhost:4317")]
pub otlp_endpoint: Url,

#[clap(global = true, long, env, default_value = "http://localhost:8787")]
pub app_endpoint: Url,
mellowagain marked this conversation as resolved.
Show resolved Hide resolved

/// Change the fpx directory.
///
/// By default fpx will search for a `.fpx` directory in the current
Expand Down
16 changes: 16 additions & 0 deletions fpx-cli/src/data/migrations/20240724_create_routes.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS app_routes (
id INTEGER PRIMARY KEY,
path TEXT,
method TEXT,
handler TEXT,
handlerType TEXT,
currentlyRegistered BOOLEAN DEFAULT FALSE,
registrationOrder INTEGER DEFAULT -1,
routeOrigin TEXT DEFAULT 'discovered',
openapiSpec TEXT,
requestType TEXT DEFAULT 'http',

-- there are no enums in sqlite so we use check statements
CONSTRAINT route_origin_check CHECK (routeOrigin IN ('discovered', 'custom', 'open_api')),
CONSTRAINT request_type_check CHECK (requestType IN ('http', 'websocket'))
);
103 changes: 102 additions & 1 deletion fpx-workers/src/data.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use axum::async_trait;
use fpx::api::models::settings::Settings;
use fpx::data::models::HexEncodedId;
use fpx::data::models::{HexEncodedId, ProbedRoutes, Route};
use fpx::data::sql::SqlBuilder;
use fpx::data::{models, DbError, Result, Store, Transaction};
use serde::Deserialize;
Expand Down Expand Up @@ -43,6 +43,29 @@ impl D1Store {
Ok(result)
}

async fn fetch_optional<T>(
mellowagain marked this conversation as resolved.
Show resolved Hide resolved
&self,
query: impl Into<String>,
values: &[JsValue],
) -> Result<Option<T>>
where
T: for<'a> Deserialize<'a>,
{
let prepared_statement = self
.database
.prepare(query)
.bind(values)
.map_err(|err| DbError::InternalError(err.to_string()))?; // TODO: Correct error;

let result = prepared_statement
.first(None)
.await
.map_err(|err| DbError::InternalError(err.to_string()))? // TODO: Correct error;
;
mellowagain marked this conversation as resolved.
Show resolved Hide resolved

Ok(result)
}

async fn fetch_all<T>(&self, query: impl Into<String>, values: &[JsValue]) -> Result<Vec<T>>
where
T: for<'a> Deserialize<'a>,
Expand Down Expand Up @@ -281,4 +304,82 @@ impl Store for D1Store {
})
.await
}

async fn routes_get(&self, _tx: &Transaction) -> Result<Vec<Route>> {
SendFuture::new(async {
let routes = self.fetch_all(&self.sql_builder.routes_get(), &[]).await?;

Ok(routes)
})
.await
}

async fn route_insert(&self, _tx: &Transaction, route: Route) -> Result<Route> {
SendFuture::new(async {
self.fetch_one(
&self.sql_builder.routes_insert(),
&[
route.id.into(),
route.path.into(),
route.method.into(),
route.handler.into(),
route.handler_type.into(),
route.currently_registered.into(),
route.registration_order.into(),
route.route_origin.to_string().into(),
route.openapi_spec.into(),
route.request_type.to_string().into(),
],
)
.await
})
.await
}

async fn route_delete(
&self,
_tx: &Transaction,
method: &str,
path: &str,
) -> Result<Option<Route>> {
SendFuture::new(async {
self.fetch_optional(
&self.sql_builder.routes_delete(),
&[method.into(), path.into()],
)
.await
})
.await
}

async fn probed_route_upsert(
&self,
_tx: &Transaction,
routes: ProbedRoutes,
) -> Result<ProbedRoutes> {
SendFuture::new(async {
let mut results = Vec::with_capacity(routes.routes.len());

for route in routes.routes {
// this gets coerced into a `ProbedRoute` instead of a `Route`
// but it should be fine as serde just ignores not-found fields
// methinks at least
results.push(
self.fetch_one(
&self.sql_builder.probed_route_upsert(),
&[
route.path.into(),
route.method.into(),
route.handler.into(),
route.handler_type.into(),
],
)
.await?,
);
}

Ok(ProbedRoutes { routes: results })
})
.await
}
}
6 changes: 5 additions & 1 deletion fpx/src/api.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::api::handlers::routes::{route_create, route_delete, route_get, route_probe};
use crate::data::BoxedStore;
use crate::otel::OtelTraceLayer;
use crate::service::Service;
use axum::extract::FromRef;
use axum::routing::{get, post};
use axum::routing::{delete, get, post};
use http::StatusCode;
use tower_http::compression::CompressionLayer;
use tower_http::cors::{Any, CorsLayer};
Expand Down Expand Up @@ -79,6 +80,9 @@ impl Builder {
"/v0/settings",
get(handlers::settings::settings_get).post(handlers::settings::settings_upsert),
)
.route("/v0/probed-routes", post(route_probe))
.route("/v0/app-routes", get(route_get).post(route_create))
.route("/v0/app-routes/:method/:path", delete(route_delete))
.with_state(api_state)
.fallback(StatusCode::NOT_FOUND)
.layer(OtelTraceLayer::default())
Expand Down
1 change: 1 addition & 0 deletions fpx/src/api/handlers.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod otel;
pub mod routes;
pub mod settings;
pub mod spans;
pub mod traces;
122 changes: 122 additions & 0 deletions fpx/src/api/handlers/routes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use crate::api::errors::{ApiServerError, CommonError};
use crate::data::models::{AllRoutes, ProbedRoutes, Route};
use crate::data::{BoxedStore, DbError};
use axum::extract::{Path, State};
use axum::Json;
use fpx_macros::ApiError;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::error;
use tracing::instrument;

#[instrument(skip(store))]
pub async fn route_get(
State(store): State<BoxedStore>,
) -> Result<Json<AllRoutes>, ApiServerError<RoutesGetError>> {
let tx = store.start_readonly_transaction().await?;

let routes = store.routes_get(&tx).await?;

store.commit_transaction(tx).await?;

Ok(Json(AllRoutes {
base_url: "http://localhost:8787".to_string(), // todo: get the right base url
mellowagain marked this conversation as resolved.
Show resolved Hide resolved
routes,
}))
}

#[derive(Debug, Serialize, Deserialize, Error, ApiError)]
#[serde(tag = "error", content = "details", rename_all = "camelCase")]
#[non_exhaustive]
pub enum RoutesGetError {}

impl From<DbError> for ApiServerError<RoutesGetError> {
fn from(err: DbError) -> Self {
error!(?err, "Failed to get all routes from db");
ApiServerError::CommonError(CommonError::InternalServerError)
}
}

#[instrument(skip_all)]
pub async fn route_probe(
State(store): State<BoxedStore>,
Json(payload): Json<ProbedRoutes>,
) -> Result<&'static str, ApiServerError<RouteProbeError>> {
if payload.routes.is_empty() {
return Ok("OK");
}

let tx = store.start_readwrite_transaction().await?;

store.probed_route_upsert(&tx, payload).await?;

store.commit_transaction(tx).await?;

// TODO: send out ws message informing studio that there are new routes

Ok("OK")
}

#[derive(Debug, Serialize, Deserialize, Error, ApiError)]
#[serde(tag = "error", content = "details", rename_all = "camelCase")]
#[non_exhaustive]
pub enum RouteProbeError {}

impl From<DbError> for ApiServerError<RouteProbeError> {
fn from(err: DbError) -> Self {
error!(?err, "Failed to insert probed routes into db");
ApiServerError::CommonError(CommonError::InternalServerError)
}
}

#[instrument(skip(store))]
pub async fn route_create(
State(store): State<BoxedStore>,
Json(route): Json<Route>, // TODO: the ts api also accepts a array of routes as input, have to figure out a Either-style way to do that
mellowagain marked this conversation as resolved.
Show resolved Hide resolved
) -> Result<Json<Route>, ApiServerError<RouteCreateError>> {
let tx = store.start_readwrite_transaction().await?;

let route = store.route_insert(&tx, route).await?;

store.commit_transaction(tx).await?;

Ok(Json(route))
}

#[derive(Debug, Serialize, Deserialize, Error, ApiError)]
#[serde(tag = "error", content = "details", rename_all = "camelCase")]
#[non_exhaustive]
pub enum RouteCreateError {}

impl From<DbError> for ApiServerError<RouteCreateError> {
fn from(err: DbError) -> Self {
error!(?err, "Failed to create rows inside db");
ApiServerError::CommonError(CommonError::InternalServerError)
}
}

#[instrument(skip(store))]
pub async fn route_delete(
State(store): State<BoxedStore>,
Path((method, path)): Path<(String, String)>,
) -> Result<Json<Option<Route>>, ApiServerError<RouteDeleteError>> {
let tx = store.start_readwrite_transaction().await?;

let route = store.route_delete(&tx, &method, &path).await?;

store.commit_transaction(tx).await?;

Ok(Json(route))
}

#[derive(Debug, Serialize, Deserialize, Error, ApiError)]
#[serde(tag = "error", content = "details", rename_all = "camelCase")]
#[non_exhaustive]
pub enum RouteDeleteError {}

impl From<DbError> for ApiServerError<RouteDeleteError> {
fn from(err: DbError) -> Self {
error!(?err, "Failed to delete route(s) from db");
ApiServerError::CommonError(CommonError::InternalServerError)
}
}
29 changes: 28 additions & 1 deletion fpx/src/data.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::api::models::settings::Settings;
use crate::data::models::HexEncodedId;
use crate::data::models::{HexEncodedId, ProbedRoutes, Route};
use async_trait::async_trait;
use std::sync::Arc;
use thiserror::Error;
Expand Down Expand Up @@ -92,4 +92,31 @@ pub trait Store: Send + Sync {
async fn settings_upsert(&self, tx: &Transaction, settings: Settings) -> Result<Settings>;

async fn settings_get(&self, tx: &Transaction) -> Result<Settings>;

async fn routes_get(&self, tx: &Transaction) -> Result<Vec<Route>>;

async fn routes_insert(&self, tx: &Transaction, routes: Vec<Route>) -> Result<Vec<Route>> {
let mut results = Vec::with_capacity(routes.len());

for route in routes {
results.push(self.route_insert(tx, route).await?);
}

Ok(results)
}

async fn route_insert(&self, tx: &Transaction, route: Route) -> Result<Route>;

async fn route_delete(
&self,
tx: &Transaction,
method: &str,
path: &str,
) -> Result<Option<Route>>;

async fn probed_route_upsert(
&self,
tx: &Transaction,
routes: ProbedRoutes,
) -> Result<ProbedRoutes>;
}
Loading
Loading