Working through airport query params
This commit is contained in:
@@ -1,233 +0,0 @@
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::db;
|
||||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::postgres::types::PgPoint;
|
||||
use crate::error::ApiResult;
|
||||
|
||||
const TABLE_NAME: &str = "airports";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
|
||||
struct RunwayDb {
|
||||
pub icao: String,
|
||||
pub id: String,
|
||||
pub length_ft: f32,
|
||||
pub width_ft: f32,
|
||||
pub surface: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AirportFilter {
|
||||
pub icaos: Option<Vec<String>>,
|
||||
pub name: Option<String>,
|
||||
// pub bounds: Option<Polygon<Point>>,
|
||||
pub categories: Option<Vec<AirportCategory>>,
|
||||
pub has_metar: Option<bool>,
|
||||
}
|
||||
|
||||
impl Default for AirportFilter {
|
||||
fn default() -> Self {
|
||||
AirportFilter {
|
||||
icaos: None,
|
||||
name: None,
|
||||
// bounds: None,
|
||||
categories: None,
|
||||
has_metar: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AirportDb {
|
||||
pub async fn find_all(_filter: &AirportFilter, _limit: i32, _page: i32) -> ApiResult<Vec<Self>> {
|
||||
let pool = db::pool();
|
||||
let airports: Vec<Self> = sqlx::query_as::<_, Self>(&format!(
|
||||
"SELECT * FROM {}",
|
||||
TABLE_NAME
|
||||
))
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
Ok(airports)
|
||||
}
|
||||
|
||||
pub async fn count(_filter: &AirportFilter) -> ApiResult<i64> {
|
||||
let pool = db::pool();
|
||||
let count: i64 = sqlx::query_scalar::<_, i64>(&format!(
|
||||
"SELECT COUNT(*) FROM {}",
|
||||
TABLE_NAME
|
||||
))
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
// fn build_query<'a>(
|
||||
// mut query: QueryBuilder<'a, Postgres>,
|
||||
// filter: &'a AirportFilter,
|
||||
// ) -> QueryBuilder<'a, Postgres> {
|
||||
// if let Some(bounds) = &filter.bounds {
|
||||
// // convert bounds to a WKT polygon
|
||||
// if bounds.rings.len() > 1 {
|
||||
// return Err(ApiError {
|
||||
// status: 400,
|
||||
// message: "Only one polygon is allowed".to_string(),
|
||||
// });
|
||||
// } else {
|
||||
// let mut points: Vec<String> = vec![];
|
||||
// bounds.rings.iter().for_each(|ring| {
|
||||
// ring.iter().for_each(|point| {
|
||||
// points.push(format!("{} {}", point.get_x(), point.get_y()));
|
||||
// });
|
||||
// });
|
||||
// let bounds = format!("POLYGON(({}))", points.join(","));
|
||||
// query.push(format!(
|
||||
// "ST_Contains(ST_GeomFromText('{}', 4326), point)",
|
||||
// bounds
|
||||
// ));
|
||||
// }
|
||||
// }
|
||||
// if let Some(categories) = &filter.categories {
|
||||
// query.push(format!(
|
||||
// "({})",
|
||||
// categories
|
||||
// .iter()
|
||||
// .map(|category| format!("category = '{}'", category.to_string()))
|
||||
// .collect::<Vec<String>>()
|
||||
// .join(" OR ")
|
||||
// ));
|
||||
// }
|
||||
//
|
||||
// fn sanitize_icao(icao: &str) -> String {
|
||||
// // Sanitize search to only allow [a-zA-Z0-9-\\s]
|
||||
// icao
|
||||
// .chars()
|
||||
// .filter(|c| c.is_alphanumeric() || *c == '-' || *c == ' ')
|
||||
// .collect::<String>()
|
||||
// }
|
||||
//
|
||||
// if &filter.icaos.is_some() == &true && &filter.name.is_some() == &true {
|
||||
// let icaos = filter.icaos.as_ref().unwrap();
|
||||
// let name = sanitize_icao(filter.name.as_ref().unwrap());
|
||||
// let icao_part = format!(
|
||||
// "({})",
|
||||
// icaos
|
||||
// .iter()
|
||||
// .map(|icao| format!("icao ILIKE '{}'", sanitize_icao(icao)))
|
||||
// .collect::<Vec<String>>()
|
||||
// .join(" OR ")
|
||||
// );
|
||||
// let name_part = format!("name ILIKE '%{}%'", name);
|
||||
// parts.push(format!("({} OR {})", icao_part, name_part));
|
||||
// } else if let Some(icaos) = &filter.icaos {
|
||||
// parts.push(format!(
|
||||
// "({})",
|
||||
// icaos
|
||||
// .iter()
|
||||
// .map(|icao| format!("icao ILIKE '{}'", sanitize_icao(icao)))
|
||||
// .collect::<Vec<String>>()
|
||||
// .join(" OR ")
|
||||
// ));
|
||||
// } else if let Some(name) = &filter.name {
|
||||
// let search = sanitize_icao(name);
|
||||
// parts.push(format!("name ILIKE '%{}%'", search));
|
||||
// }
|
||||
// if let Some(has_metar) = &filter.has_metar {
|
||||
// parts.push(format!("has_metar = {}", has_metar));
|
||||
// }
|
||||
//
|
||||
// if parts.len() > 0 {
|
||||
// query = format!("{} WHERE {}", query, parts.join(" AND "));
|
||||
// }
|
||||
//
|
||||
// return Ok(query);
|
||||
// }
|
||||
|
||||
pub async fn find_by_icao(icao: &str) -> ApiResult<Self> {
|
||||
let pool = db::pool();
|
||||
let airport =
|
||||
sqlx::query_as::<_, Self>(&format!("SELECT * FROM {} WHERE icao = $1", TABLE_NAME))
|
||||
.bind(icao)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
Ok(airport)
|
||||
}
|
||||
|
||||
pub async fn insert(&self) -> ApiResult<()> {
|
||||
let pool = db::pool();
|
||||
sqlx::query(&format!(
|
||||
"INSERT INTO {} (
|
||||
icao,
|
||||
category,
|
||||
name,
|
||||
elevation_ft,
|
||||
iso_country,
|
||||
iso_region,
|
||||
municipality,
|
||||
has_metar,
|
||||
point,
|
||||
data
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
|
||||
)",
|
||||
TABLE_NAME
|
||||
))
|
||||
.bind(self.icao.clone())
|
||||
.bind(self.category.clone())
|
||||
.bind(&self.name)
|
||||
.bind(self.elevation_ft)
|
||||
.bind(self.iso_country.clone())
|
||||
.bind(self.iso_region.clone())
|
||||
.bind(self.municipality.clone())
|
||||
.bind(self.has_metar.clone())
|
||||
// .bind(self.point.clone())
|
||||
.bind(self.data.clone())
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// pub fn insert_vec(airports: Vec<Self>) -> ApiResult<Vec<Self>> {
|
||||
// let mut conn: r2d2::PooledConnection<diesel::r2d2::ConnectionManager<PgConnection>> =
|
||||
// db::connection()?;
|
||||
// let mut inserted_airports: Vec<Self> = vec![];
|
||||
// for airport in airports {
|
||||
// let airport = Self::from(airport);
|
||||
// let airport = diesel::insert_into(airports::table)
|
||||
// .values(airport)
|
||||
// .on_conflict_do_nothing()
|
||||
// .get_result(&mut conn)?;
|
||||
// inserted_airports.push(airport);
|
||||
// }
|
||||
// Ok(inserted_airports)
|
||||
// }
|
||||
|
||||
pub async fn update(&self) -> ApiResult<()> {
|
||||
// let mut conn = db::pool()?;
|
||||
// let airport = diesel::update(airports::table)
|
||||
// .filter(airports::icao.eq(airport.icao.clone()))
|
||||
// .set(airport)
|
||||
// .get_result(&mut conn)?;
|
||||
// Ok(airport)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_all() -> ApiResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_by_icao(_icao: &str) -> ApiResult<()> {
|
||||
// let mut conn = db::pool()?;
|
||||
// let res = match icao {
|
||||
// Some(icao) => {
|
||||
// diesel::delete(airports::table.filter(airports::icao.eq(icao))).execute(&mut conn)?
|
||||
// }
|
||||
// None => diesel::delete(airports::table).execute(&mut conn)?,
|
||||
// };
|
||||
// Ok(res)
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::str::FromStr;
|
||||
use actix_web::web::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Postgres, QueryBuilder};
|
||||
use sqlx::{Execute, Postgres, QueryBuilder};
|
||||
use crate::airports::model::airport_category::AirportCategory;
|
||||
use crate::airports::{Frequency, Runway, UpdateFrequency, UpdateRunway};
|
||||
use crate::db;
|
||||
@@ -33,7 +33,38 @@ pub struct Airport {
|
||||
pub public: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AirportQuery {
|
||||
pub page: Option<u32>,
|
||||
pub limit: Option<u32>,
|
||||
pub icaos: Option<String>,
|
||||
pub iatas: Option<String>,
|
||||
pub locals: Option<String>,
|
||||
pub names: Option<String>,
|
||||
pub categories: Option<String>,
|
||||
pub iso_countries: Option<String>,
|
||||
pub iso_regions: Option<String>,
|
||||
pub municipalities: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for AirportQuery {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
page: Some(1),
|
||||
limit: Some(1000),
|
||||
icaos: None,
|
||||
iatas: None,
|
||||
locals: None,
|
||||
names: None,
|
||||
categories: None,
|
||||
iso_countries: None,
|
||||
iso_regions: None,
|
||||
municipalities: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, sqlx::FromRow)]
|
||||
struct AirportRow {
|
||||
pub icao: String,
|
||||
pub iata: Option<String>,
|
||||
@@ -51,39 +82,23 @@ struct AirportRow {
|
||||
pub public: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdateAirport {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icao: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub iata: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub local: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub category: Option<AirportCategory>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub iso_country: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub iso_region: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub municipality: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub elevation_ft: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub longitude: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub latitude: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub has_tower: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub has_beacon: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub runways: Option<Vec<UpdateRunway>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub frequencies: Option<Vec<UpdateFrequency>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub public: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -161,19 +176,95 @@ impl Airport {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn select_all() -> ApiResult<Vec<Self>> {
|
||||
pub async fn select_all(query: &AirportQuery) -> ApiResult<Vec<Self>> {
|
||||
let pool = db::pool();
|
||||
|
||||
let airports: Vec<AirportRow> = sqlx::query_as(&format!(
|
||||
r#"
|
||||
SELECT * FROM {}
|
||||
"#,
|
||||
TABLE_NAME
|
||||
))
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
let mut builder = QueryBuilder::<Postgres>::new("SELECT * FROM ");
|
||||
builder.push(TABLE_NAME);
|
||||
|
||||
Ok(airports.into_iter().map(From::from).collect())
|
||||
let mut has_where = false;
|
||||
macro_rules! push_condition {
|
||||
($field:expr, $value:expr) => {
|
||||
if let Some(ref val) = $value {
|
||||
if !has_where {
|
||||
builder.push(" WHERE ");
|
||||
has_where = true;
|
||||
} else {
|
||||
builder.push(" AND ");
|
||||
}
|
||||
builder.push($field).push(" = ").push_bind(val);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// push_condition!("icao", query.icaos);
|
||||
// push_condition!("iata", query.iata);
|
||||
// push_condition!("iso_country", query.iso_country);
|
||||
// push_condition!("iso_region", query.iso_region);
|
||||
// push_condition!("municipality", query.municipality);
|
||||
|
||||
// Apply pagination.
|
||||
if let Some(limit) = query.limit {
|
||||
builder.push(" LIMIT ").push_bind(limit as i64);
|
||||
let offset = if let Some(page) = query.page {
|
||||
// Calculate offset (page is 1-based).
|
||||
(page.saturating_sub(1) * limit) as i64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
builder.push(" OFFSET ").push_bind(offset);
|
||||
}
|
||||
|
||||
let query = builder.build_query_as();
|
||||
let airport_rows: Vec<AirportRow> = query.fetch_all(pool).await?;
|
||||
Ok(airport_rows.into_iter().map(From::from).collect())
|
||||
}
|
||||
|
||||
pub async fn count(query: &AirportQuery) -> i64 {
|
||||
let pool = db::pool();
|
||||
|
||||
let mut builder = QueryBuilder::<Postgres>::new("SELECT COUNT(*) FROM ");
|
||||
builder.push(TABLE_NAME);
|
||||
|
||||
let mut has_where = false;
|
||||
macro_rules! push_condition_array {
|
||||
($column:expr, $field:expr) => {
|
||||
if let Some(ref value_str) = $field {
|
||||
// split on commas, trim whitespace, and drop empties
|
||||
let values: Vec<&str> = value_str
|
||||
.split(',')
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
if !values.is_empty() {
|
||||
if !has_where {
|
||||
builder.push(" WHERE ");
|
||||
has_where = true;
|
||||
} else {
|
||||
builder.push(" AND ");
|
||||
}
|
||||
dbg!(&values);
|
||||
builder.push($column);
|
||||
builder.push(" = ANY(");
|
||||
builder.push_bind(values);
|
||||
builder.push(")");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
push_condition_array!("icao", query.icaos);
|
||||
push_condition_array!("iata", query.iatas);
|
||||
push_condition_array!("iso_country", query.iso_countries);
|
||||
push_condition_array!("iso_region", query.iso_regions);
|
||||
push_condition_array!("municipality", query.municipalities);
|
||||
push_condition_array!("local", query.locals);
|
||||
push_condition_array!("name", query.names);
|
||||
push_condition_array!("category", query.categories);
|
||||
|
||||
let sql_query = builder.build_query_scalar();
|
||||
dbg!(&sql_query.sql());
|
||||
sql_query.fetch_one(pool).await.unwrap_or_else(|_| 0)
|
||||
}
|
||||
|
||||
pub async fn insert(&self) -> ApiResult<Self> {
|
||||
|
||||
@@ -9,22 +9,9 @@ use crate::{
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::{delete, get, post, put, web, HttpResponse, HttpRequest, ResponseError};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::airports::UpdateAirport;
|
||||
use crate::airports::{AirportQuery, UpdateAirport};
|
||||
use crate::users::ADMIN_ROLE;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct AirportsQuery {
|
||||
icaos: Option<String>,
|
||||
name: Option<String>,
|
||||
bounds: Option<String>,
|
||||
categories: Option<String>,
|
||||
order_field: Option<String>,
|
||||
order_by: Option<String>,
|
||||
has_metar: Option<String>,
|
||||
limit: Option<i32>,
|
||||
page: Option<i32>,
|
||||
}
|
||||
|
||||
#[post("/import")]
|
||||
async fn import_airports(mut payload: Multipart, auth: Auth) -> HttpResponse {
|
||||
if let Err(err) = verify_role(&auth, ADMIN_ROLE) {
|
||||
@@ -69,8 +56,27 @@ async fn import_airports(mut payload: Multipart, auth: Auth) -> HttpResponse {
|
||||
|
||||
#[get("")]
|
||||
async fn get_airports(req: HttpRequest) -> HttpResponse {
|
||||
match Airport::select_all().await {
|
||||
Ok(airports) => HttpResponse::Ok().json(airports),
|
||||
let mut query = match web::Query::<AirportQuery>::from_query(req.query_string()) {
|
||||
Ok(q) => q.into_inner(),
|
||||
Err(_) => AirportQuery::default(),
|
||||
};
|
||||
|
||||
let total = Airport::count(&query).await;
|
||||
let page = query.page.unwrap_or(1);
|
||||
let mut limit = query.limit.unwrap_or(total as u32);
|
||||
if limit > 1000 {
|
||||
limit = 1000
|
||||
}
|
||||
query.limit = Some(limit);
|
||||
query.page = Some(page);
|
||||
|
||||
match Airport::select_all(&query).await {
|
||||
Ok(airports) => HttpResponse::Ok().json(Paged {
|
||||
data: airports,
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
}),
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
ResponseError::error_response(&err)
|
||||
|
||||
Reference in New Issue
Block a user