mirror of
https://github.com/trailbaseio/trailbase.git
synced 2026-02-19 08:10:11 -06:00
Add support for parsing all the search query parameters for listing.
This commit is contained in:
2
Makefile
2
Makefile
@@ -26,6 +26,6 @@ docker:
|
||||
docker buildx build --platform linux/arm64,linux/amd64 --output=type=registry -t trailbase/trailbase:latest .
|
||||
|
||||
cloc:
|
||||
cloc --not-match-d=".*(/target|/dist|/node_modules|/vendor|.astro|.venv|/traildepot|/flutter|/assets|lock|_benchmark|/bin|/obj).*" .
|
||||
cloc --not-match-d=".*(/target|/dist|/node_modules|/vendor|.astro|.build|.venv|/traildepot|/flutter|/assets|lock|_benchmark|/bin|/obj).*" .
|
||||
|
||||
.PHONY: default format check static docker cloc
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use serde::de::{Deserializer, Error};
|
||||
|
||||
use crate::value::{Value, unexpected};
|
||||
use crate::value::Value;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum CompareOp {
|
||||
@@ -55,11 +55,14 @@ impl<'de> serde::de::Deserialize<'de> for CompareOp {
|
||||
let value = Value::deserialize(deserializer)?;
|
||||
|
||||
let Value::String(ref string) = value else {
|
||||
return Err(Error::invalid_type(unexpected(&value), &"String"));
|
||||
return Err(Error::invalid_type(
|
||||
crate::util::unexpected(&value),
|
||||
&"String",
|
||||
));
|
||||
};
|
||||
|
||||
return CompareOp::from(string)
|
||||
.ok_or_else(|| Error::invalid_type(unexpected(&value), &"(eq|ne)"));
|
||||
.ok_or_else(|| Error::invalid_type(crate::util::unexpected(&value), &"(eq|ne)"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +93,9 @@ where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
use serde_value::Value;
|
||||
if !crate::util::sanitize_column_name(&key) {
|
||||
return Err(Error::custom(format!("invalid column name: {key}")));
|
||||
}
|
||||
|
||||
return match value {
|
||||
Value::String(_) => Ok(ColumnOpValue {
|
||||
@@ -109,7 +115,7 @@ where
|
||||
})
|
||||
}
|
||||
v => Err(Error::invalid_type(
|
||||
unexpected(&v),
|
||||
crate::util::unexpected(&v),
|
||||
&"Map<String, String | Map<Rel, String>>",
|
||||
)),
|
||||
};
|
||||
@@ -128,7 +134,7 @@ impl<'de> serde::de::Deserialize<'de> for ColumnOpValueMap {
|
||||
|
||||
let Value::Map(m) = value else {
|
||||
return Err(Error::invalid_type(
|
||||
unexpected(&value),
|
||||
crate::util::unexpected(&value),
|
||||
&"Map<String, String | Map<Rel, String>>",
|
||||
));
|
||||
};
|
||||
@@ -138,7 +144,7 @@ impl<'de> serde::de::Deserialize<'de> for ColumnOpValueMap {
|
||||
.map(|(k, v)| {
|
||||
return match (k, v) {
|
||||
(Value::String(key), v) => serde_value_to_single_column_rel_value::<D>(key, v),
|
||||
(k, _) => Err(Error::invalid_type(unexpected(&k), &"String")),
|
||||
(k, _) => Err(Error::invalid_type(crate::util::unexpected(&k), &"String")),
|
||||
};
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
@@ -11,7 +11,6 @@ use itertools::Itertools;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::column_rel_value::{ColumnOpValue, serde_value_to_single_column_rel_value};
|
||||
use crate::value::unexpected;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Combiner {
|
||||
@@ -60,7 +59,10 @@ where
|
||||
|
||||
// We always expect [key] = value, i.e. a Map[key] = value.
|
||||
let Value::Map(mut m) = value else {
|
||||
return Err(Error::invalid_type(unexpected(&value), &"Map1"));
|
||||
return Err(Error::invalid_type(
|
||||
crate::util::unexpected(&value),
|
||||
&"map[col]=val or map[col][op]=val",
|
||||
));
|
||||
};
|
||||
|
||||
if m.is_empty() {
|
||||
@@ -81,7 +83,10 @@ where
|
||||
serde_value_to_single_column_rel_value::<D>(key, v)?,
|
||||
)),
|
||||
},
|
||||
_ => Err(Error::invalid_type(unexpected(&k), &"Map0")),
|
||||
_ => Err(Error::invalid_type(
|
||||
crate::util::unexpected(&k),
|
||||
&"string key",
|
||||
)),
|
||||
};
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
@@ -107,7 +112,10 @@ where
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
));
|
||||
}
|
||||
v => Err(Error::invalid_type(unexpected(&v), &"Sequence")),
|
||||
v => Err(Error::invalid_type(
|
||||
crate::util::unexpected(&v),
|
||||
&"Sequence",
|
||||
)),
|
||||
}
|
||||
};
|
||||
|
||||
@@ -120,7 +128,7 @@ where
|
||||
serde_value_to_single_column_rel_value::<D>(str, v)?,
|
||||
)),
|
||||
},
|
||||
(k, _) => Err(Error::invalid_type(unexpected(&k), &"String")),
|
||||
(k, _) => Err(Error::invalid_type(crate::util::unexpected(&k), &"String")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,40 +228,5 @@ mod tests {
|
||||
let m3: Result<Query, _> =
|
||||
qs.deserialize_str("composite_filter[col0]=val0&composite_filter[$and][0][col0]=val0&composite_filter[col1]=val1");
|
||||
assert!(m3.is_err(), "{m3:?}");
|
||||
|
||||
// Implicit and with nested or and out of order.
|
||||
let m5: Query = qs.deserialize_str("composite_filter[$or][1][col0][ne]=val0&composite_filter[col1]=1&composite_filter[$or][0][col2]=val2").unwrap();
|
||||
assert_eq!(
|
||||
m5.composite_filter.as_ref().unwrap(),
|
||||
&ValueOrComposite::Composite(
|
||||
Combiner::And,
|
||||
vec![
|
||||
ValueOrComposite::Composite(
|
||||
Combiner::Or,
|
||||
vec![
|
||||
ValueOrComposite::Value(ColumnOpValue {
|
||||
column: "col2".to_string(),
|
||||
op: CompareOp::Equal,
|
||||
value: Value::String("val2".to_string()),
|
||||
}),
|
||||
ValueOrComposite::Value(ColumnOpValue {
|
||||
column: "col0".to_string(),
|
||||
op: CompareOp::NotEqual,
|
||||
value: Value::String("val0".to_string()),
|
||||
}),
|
||||
]
|
||||
),
|
||||
ValueOrComposite::Value(ColumnOpValue {
|
||||
column: "col1".to_string(),
|
||||
op: CompareOp::Equal,
|
||||
value: Value::Integer(1),
|
||||
}),
|
||||
]
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
m5.composite_filter.unwrap().to_sql(),
|
||||
"((col2 = 'val2' OR col0 <> 'val0') AND col1 = 1)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,6 @@
|
||||
|
||||
mod column_rel_value;
|
||||
mod filter;
|
||||
mod query;
|
||||
mod util;
|
||||
mod value;
|
||||
|
||||
272
trailbase-qs/src/query.rs
Normal file
272
trailbase-qs/src/query.rs
Normal file
@@ -0,0 +1,272 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::filter::ValueOrComposite;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
pub enum Cursor {
|
||||
Blob(Vec<u8>),
|
||||
Integer(i64),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum OrderPrecedent {
|
||||
Ascending,
|
||||
Descending,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Order {
|
||||
columns: Vec<(String, OrderPrecedent)>,
|
||||
}
|
||||
|
||||
impl<'de> serde::de::Deserialize<'de> for Order {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
use serde_value::Value;
|
||||
let value = Value::deserialize(deserializer)?;
|
||||
|
||||
let Value::String(str) = value else {
|
||||
return Err(Error::invalid_type(
|
||||
crate::util::unexpected(&value),
|
||||
&"comma separated column names",
|
||||
));
|
||||
};
|
||||
|
||||
let columns = str
|
||||
.split(",")
|
||||
.map(|v| {
|
||||
let col_order = match v.trim() {
|
||||
x if x.starts_with("-") => (v[1..].to_string(), OrderPrecedent::Descending),
|
||||
x if x.starts_with("+") => (v[1..].to_string(), OrderPrecedent::Ascending),
|
||||
x => (x.to_string(), OrderPrecedent::Ascending),
|
||||
};
|
||||
|
||||
if !crate::util::sanitize_column_name(&col_order.0) {
|
||||
return Err(Error::custom(format!(
|
||||
"invalid column name: {}",
|
||||
col_order.0
|
||||
)));
|
||||
}
|
||||
|
||||
return Ok(col_order);
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
if columns.len() > 5 {
|
||||
return Err(Error::invalid_length(
|
||||
5,
|
||||
&"more more than 5 order dimension",
|
||||
));
|
||||
}
|
||||
|
||||
return Ok(Order { columns });
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Expand {
|
||||
columns: Vec<String>,
|
||||
}
|
||||
|
||||
impl<'de> serde::de::Deserialize<'de> for Expand {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
use serde_value::Value;
|
||||
let value = Value::deserialize(deserializer)?;
|
||||
|
||||
let Value::String(str) = value else {
|
||||
return Err(Error::invalid_type(
|
||||
crate::util::unexpected(&value),
|
||||
&"comma separated column names",
|
||||
));
|
||||
};
|
||||
|
||||
let columns = str
|
||||
.split(",")
|
||||
.map(|column_name| {
|
||||
if !crate::util::sanitize_column_name(column_name) {
|
||||
return Err(Error::custom(
|
||||
format!("invalid column name: {column_name}",),
|
||||
));
|
||||
}
|
||||
|
||||
return Ok(column_name.to_string());
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
if columns.len() > 5 {
|
||||
return Err(Error::invalid_length(
|
||||
5,
|
||||
&"more more than 5 expand dimension",
|
||||
));
|
||||
}
|
||||
|
||||
return Ok(Expand { columns });
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq, Deserialize)]
|
||||
pub struct Query {
|
||||
/// Pagination parameters:
|
||||
///
|
||||
/// Max number of elements returned per page.
|
||||
pub limit: Option<usize>,
|
||||
/// Cursor to page.
|
||||
pub cursor: Option<String>,
|
||||
/// Offset to page. Cursor is more efficient when available
|
||||
pub offset: Option<usize>,
|
||||
|
||||
/// Return total number of rows in the table.
|
||||
pub count: Option<bool>,
|
||||
|
||||
/// Which foreign key columns to expand (only when allowed by configuration).
|
||||
pub expand: Option<Expand>,
|
||||
|
||||
/// Ordering. It's a vector for &order=-col0,+col1,col2
|
||||
pub order: Option<Order>,
|
||||
|
||||
/// Map from filter params to filter value. It's a vector in cases like:
|
||||
/// `col0[gte]=2&col0[lte]=10`.
|
||||
pub filter: Option<ValueOrComposite>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use serde_qs::Config;
|
||||
|
||||
use crate::column_rel_value::{ColumnOpValue, CompareOp};
|
||||
use crate::filter::Combiner;
|
||||
use crate::value::Value;
|
||||
|
||||
#[test]
|
||||
fn test_query_basic_parsing() {
|
||||
let qs = Config::new(5, true);
|
||||
|
||||
assert_eq!(qs.deserialize_str::<Query>("").unwrap(), Query::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_order_parsing() {
|
||||
let qs = Config::new(5, true);
|
||||
|
||||
assert_eq!(
|
||||
qs.deserialize_str::<Query>("order=").unwrap(),
|
||||
Query {
|
||||
order: None,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert!(qs.deserialize_str::<Query>("order=$").is_err());
|
||||
assert!(qs.deserialize_str::<Query>("order=a,b,c,d,e").is_ok());
|
||||
assert!(qs.deserialize_str::<Query>("order=a,b,c,d,e,f").is_err());
|
||||
|
||||
assert_eq!(
|
||||
qs.deserialize_str::<Query>("order=a,-b,+c").unwrap(),
|
||||
Query {
|
||||
order: Some(Order {
|
||||
columns: vec![
|
||||
("a".to_string(), OrderPrecedent::Ascending),
|
||||
("b".to_string(), OrderPrecedent::Descending),
|
||||
("c".to_string(), OrderPrecedent::Ascending),
|
||||
]
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_expand_parsing() {
|
||||
let qs = Config::new(5, true);
|
||||
|
||||
assert_eq!(
|
||||
qs.deserialize_str::<Query>("expand=").unwrap(),
|
||||
Query {
|
||||
expand: None,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert!(qs.deserialize_str::<Query>("expand=$").is_err());
|
||||
assert!(qs.deserialize_str::<Query>("expand=a,b,c,d,e").is_ok());
|
||||
assert!(qs.deserialize_str::<Query>("expand=a,b,c,d,e,f").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_filter_parsing() {
|
||||
let qs = Config::new(5, true);
|
||||
|
||||
assert_eq!(
|
||||
qs.deserialize_str::<Query>("filter=").unwrap(),
|
||||
Query::default()
|
||||
);
|
||||
|
||||
let q0: Query = qs
|
||||
.deserialize_str("filter[col0][gt]=0&filter[col1]=val1")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
q0.filter.unwrap(),
|
||||
ValueOrComposite::Composite(
|
||||
Combiner::And,
|
||||
vec![
|
||||
ValueOrComposite::Value(ColumnOpValue {
|
||||
column: "col0".to_string(),
|
||||
op: CompareOp::GreaterThan,
|
||||
value: Value::Integer(0),
|
||||
}),
|
||||
ValueOrComposite::Value(ColumnOpValue {
|
||||
column: "col1".to_string(),
|
||||
op: CompareOp::Equal,
|
||||
value: Value::String("val1".to_string()),
|
||||
}),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
// Implicit and with nested or and out of order.
|
||||
let q1: Query = qs
|
||||
.deserialize_str("filter[$or][1][col0][ne]=val0&filter[col1]=1&filter[$or][0][col2]=val2")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
q1.filter.as_ref().unwrap(),
|
||||
&ValueOrComposite::Composite(
|
||||
Combiner::And,
|
||||
vec![
|
||||
ValueOrComposite::Composite(
|
||||
Combiner::Or,
|
||||
vec![
|
||||
ValueOrComposite::Value(ColumnOpValue {
|
||||
column: "col2".to_string(),
|
||||
op: CompareOp::Equal,
|
||||
value: Value::String("val2".to_string()),
|
||||
}),
|
||||
ValueOrComposite::Value(ColumnOpValue {
|
||||
column: "col0".to_string(),
|
||||
op: CompareOp::NotEqual,
|
||||
value: Value::String("val0".to_string()),
|
||||
}),
|
||||
]
|
||||
),
|
||||
ValueOrComposite::Value(ColumnOpValue {
|
||||
column: "col1".to_string(),
|
||||
op: CompareOp::Equal,
|
||||
value: Value::Integer(1),
|
||||
}),
|
||||
]
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
q1.filter.unwrap().to_sql(),
|
||||
"((col2 = 'val2' OR col0 <> 'val0') AND col1 = 1)"
|
||||
);
|
||||
}
|
||||
}
|
||||
36
trailbase-qs/src/util.rs
Normal file
36
trailbase-qs/src/util.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use serde::de::Unexpected;
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn sanitize_column_name(name: &str) -> bool {
|
||||
// Assuming that all uses are quoted correctly, it should be enough to discard names containing
|
||||
// (", ', `, [, ]), however we're conservative here.
|
||||
return name
|
||||
.chars()
|
||||
.all(|c| c.is_alphanumeric() || c == '.' || c == '-' || c == '_');
|
||||
}
|
||||
|
||||
pub(crate) fn unexpected(value: &serde_value::Value) -> Unexpected {
|
||||
use serde_value::Value;
|
||||
|
||||
match *value {
|
||||
Value::Bool(b) => Unexpected::Bool(b),
|
||||
Value::U8(n) => Unexpected::Unsigned(n as u64),
|
||||
Value::U16(n) => Unexpected::Unsigned(n as u64),
|
||||
Value::U32(n) => Unexpected::Unsigned(n as u64),
|
||||
Value::U64(n) => Unexpected::Unsigned(n),
|
||||
Value::I8(n) => Unexpected::Signed(n as i64),
|
||||
Value::I16(n) => Unexpected::Signed(n as i64),
|
||||
Value::I32(n) => Unexpected::Signed(n as i64),
|
||||
Value::I64(n) => Unexpected::Signed(n),
|
||||
Value::F32(n) => Unexpected::Float(n as f64),
|
||||
Value::F64(n) => Unexpected::Float(n),
|
||||
Value::Char(c) => Unexpected::Char(c),
|
||||
Value::String(ref s) => Unexpected::Str(s),
|
||||
Value::Unit => Unexpected::Unit,
|
||||
Value::Option(_) => Unexpected::Option,
|
||||
Value::Newtype(_) => Unexpected::NewtypeStruct,
|
||||
Value::Seq(_) => Unexpected::Seq,
|
||||
Value::Map(_) => Unexpected::Map,
|
||||
Value::Bytes(ref b) => Unexpected::Bytes(b),
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use base64::prelude::*;
|
||||
use serde::de::{Deserialize, Deserializer, Error, Unexpected};
|
||||
use serde::de::{Deserialize, Deserializer, Error};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -69,7 +69,10 @@ where
|
||||
serde_value::Value::U16(i) => Ok(Value::Integer(i as i64)),
|
||||
serde_value::Value::U8(i) => Ok(Value::Integer(i as i64)),
|
||||
serde_value::Value::Bool(b) => Ok(Value::Bool(b)),
|
||||
_ => Err(Error::invalid_type(unexpected(&value), &"Value")),
|
||||
_ => Err(Error::invalid_type(
|
||||
crate::util::unexpected(&value),
|
||||
&"Value",
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -85,31 +88,6 @@ impl<'de> Deserialize<'de> for Value {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unexpected(value: &serde_value::Value) -> Unexpected {
|
||||
use serde_value::Value;
|
||||
match *value {
|
||||
Value::Bool(b) => serde::de::Unexpected::Bool(b),
|
||||
Value::U8(n) => serde::de::Unexpected::Unsigned(n as u64),
|
||||
Value::U16(n) => serde::de::Unexpected::Unsigned(n as u64),
|
||||
Value::U32(n) => serde::de::Unexpected::Unsigned(n as u64),
|
||||
Value::U64(n) => serde::de::Unexpected::Unsigned(n),
|
||||
Value::I8(n) => serde::de::Unexpected::Signed(n as i64),
|
||||
Value::I16(n) => serde::de::Unexpected::Signed(n as i64),
|
||||
Value::I32(n) => serde::de::Unexpected::Signed(n as i64),
|
||||
Value::I64(n) => serde::de::Unexpected::Signed(n),
|
||||
Value::F32(n) => serde::de::Unexpected::Float(n as f64),
|
||||
Value::F64(n) => serde::de::Unexpected::Float(n),
|
||||
Value::Char(c) => serde::de::Unexpected::Char(c),
|
||||
Value::String(ref s) => serde::de::Unexpected::Str(s),
|
||||
Value::Unit => serde::de::Unexpected::Unit,
|
||||
Value::Option(_) => serde::de::Unexpected::Option,
|
||||
Value::Newtype(_) => serde::de::Unexpected::NewtypeStruct,
|
||||
Value::Seq(_) => serde::de::Unexpected::Seq,
|
||||
Value::Map(_) => serde::de::Unexpected::Map,
|
||||
Value::Bytes(ref b) => serde::de::Unexpected::Bytes(b),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user