pub mod bracket;
pub mod coerce;
pub mod helpers;
pub mod logical;
pub mod select;
#[cfg(test)]
mod tests;
use std::collections::HashMap;
pub use bracket::parse_bracket_key;
pub use coerce::coerce_to_type;
use fraiseql_core::{
schema::{QueryDefinition, RestConfig, TypeDefinition},
utils::operators::OPERATOR_REGISTRY,
};
use fraiseql_error::FraiseQLError;
pub use helpers::{count_where_fields, field_names, json_depth, validation_error};
pub use logical::{
parse_logical_group, parse_logical_value, parse_nested_logical, split_logical_parts,
};
pub use select::{parse_select_entries, validate_embedding_depth};
const MAX_VARIABLES_COUNT: usize = 1_000;
const MAX_QUERY_STRING_BYTES: usize = 1_048_576;
const MAX_FILTER_DEPTH: usize = 64;
const BRACKET_OPERATORS: &[&str] = &[
"eq",
"ne",
"gt",
"gte",
"lt",
"lte",
"in",
"nin",
"like",
"ilike",
"icontains",
"startswith",
"istartswith",
"endswith",
"iendswith",
"is_null",
];
const RESERVED_PARAMS: &[&str] = &[
"select", "sort", "limit", "offset", "first", "after", "last", "before", "filter", "search",
"or", "and", "not",
];
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct ExtractedParams {
pub path_params: Vec<(String, serde_json::Value)>,
pub where_clause: Option<serde_json::Value>,
pub order_by: Option<serde_json::Value>,
pub pagination: PaginationParams,
pub field_selection: RestFieldSpec,
pub search_query: Option<String>,
pub embeddings: Vec<EmbeddedSpec>,
pub embedding_filters: HashMap<String, serde_json::Value>,
pub embedding_counts: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum PaginationParams {
Offset {
limit: u64,
offset: u64,
},
Cursor {
first: Option<u64>,
after: Option<String>,
last: Option<u64>,
before: Option<String>,
},
None,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum RestFieldSpec {
All,
Fields(Vec<String>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SelectEntry {
Field(String),
Embedded(EmbeddedSpec),
Count(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EmbeddedSpec {
pub relationship: String,
pub rename: Option<String>,
pub fields: Vec<SelectEntry>,
}
pub struct RestParamExtractor<'a> {
config: &'a RestConfig,
query_def: &'a QueryDefinition,
type_def: Option<&'a TypeDefinition>,
}
impl<'a> RestParamExtractor<'a> {
#[must_use]
pub const fn new(
config: &'a RestConfig,
query_def: &'a QueryDefinition,
type_def: Option<&'a TypeDefinition>,
) -> Self {
Self {
config,
query_def,
type_def,
}
}
pub fn extract(
&self,
path_pairs: &[(&str, &str)],
query_pairs: &[(&str, &str)],
) -> Result<ExtractedParams, FraiseQLError> {
let total_query_bytes: usize = query_pairs.iter().map(|(k, v)| k.len() + v.len()).sum();
if total_query_bytes > MAX_QUERY_STRING_BYTES {
return Err(validation_error(format!(
"Total query string size ({total_query_bytes} bytes) exceeds maximum \
allowed ({MAX_QUERY_STRING_BYTES} bytes)."
)));
}
let is_relay = self.query_def.relay;
let is_list = self.query_def.returns_list;
let path_params = self.extract_path_params(path_pairs)?;
let mut simple_filters: Vec<(String, serde_json::Value)> = Vec::new();
let mut bracket_filters: Vec<(String, String, String)> = Vec::new(); let mut filter_json: Option<&str> = None;
let mut select_raw: Option<&str> = None;
let mut sort_raw: Option<&str> = None;
let mut limit_raw: Option<&str> = None;
let mut offset_raw: Option<&str> = None;
let mut first_raw: Option<&str> = None;
let mut after_raw: Option<&str> = None;
let mut last_raw: Option<&str> = None;
let mut before_raw: Option<&str> = None;
let mut search_raw: Option<&str> = None;
let mut logical_groups: Vec<(&str, &str)> = Vec::new();
for &(key, value) in query_pairs {
if key.contains('.') && !RESERVED_PARAMS.contains(&key) {
continue;
}
if let Some((field, op)) = parse_bracket_key(key) {
if field.contains('.') {
continue;
}
self.validate_bracket_operator(&op)?;
self.validate_field_name(&field)?;
bracket_filters.push((field, op, value.to_string()));
} else {
match key {
"select" => select_raw = Some(value),
"sort" => sort_raw = Some(value),
"limit" => limit_raw = Some(value),
"offset" => offset_raw = Some(value),
"first" => first_raw = Some(value),
"after" => after_raw = Some(value),
"last" => last_raw = Some(value),
"before" => before_raw = Some(value),
"filter" => filter_json = Some(value),
"search" => search_raw = Some(value),
"or" | "and" | "not" => logical_groups.push((key, value)),
_ => {
if RESERVED_PARAMS.contains(&key) {
continue;
}
if is_list && self.is_valid_field(key) {
let coerced = self.coerce_field_value(key, value)?;
simple_filters.push((key.to_string(), coerced));
} else if !is_list {
return Err(validation_error(format!(
"Unknown query parameter '{key}' for single-resource endpoint. \
Available parameters: select"
)));
} else {
return Err(self.unknown_param_error(key));
}
},
}
}
}
if is_list {
let has_offset_params = limit_raw.is_some() || offset_raw.is_some();
let has_cursor_params = first_raw.is_some()
|| after_raw.is_some()
|| last_raw.is_some()
|| before_raw.is_some();
if is_relay && has_offset_params {
return Err(validation_error(
"This endpoint uses cursor-based pagination. \
Use `first`/`after`/`last`/`before` instead of `limit`/`offset`."
.to_string(),
));
}
if !is_relay && has_cursor_params {
return Err(validation_error(
"This endpoint uses offset-based pagination. \
Use `limit`/`offset` instead of `first`/`after`/`last`/`before`."
.to_string(),
));
}
}
let (field_selection, embeddings, embedding_counts) =
self.parse_select_with_embeddings(select_raw)?;
let order_by = self.parse_sort(sort_raw)?;
let search_query = if let Some(raw) = search_raw {
if !is_list {
return Err(validation_error(
"Full-text search not available on single-resource endpoints.".to_string(),
));
}
if let Some(td) = self.type_def {
if td.searchable_fields().is_empty() {
return Err(validation_error(format!(
"Full-text search not available on '{}'. \
No searchable fields configured.",
td.name.as_str()
)));
}
}
Some(raw.to_string())
} else {
None
};
let parsed_logical = self.parse_logical_groups(&logical_groups)?;
let filter_where = self.parse_filter(filter_json)?;
let where_clause = self.merge_where_with_logical(
simple_filters,
bracket_filters,
filter_where,
parsed_logical,
)?;
let pagination = if !is_list {
PaginationParams::None
} else if is_relay {
self.parse_cursor_pagination(first_raw, after_raw, last_raw, before_raw)?
} else {
self.parse_offset_pagination(limit_raw, offset_raw)?
};
let total_count = path_params.len()
+ where_clause.as_ref().map_or(0, count_where_fields)
+ order_by.as_ref().map_or(0, |_| 1)
+ match &pagination {
PaginationParams::Offset { .. } => 2,
PaginationParams::Cursor {
first,
after,
last,
before,
} => {
usize::from(first.is_some())
+ usize::from(after.is_some())
+ usize::from(last.is_some())
+ usize::from(before.is_some())
},
PaginationParams::None => 0,
}
+ match &field_selection {
RestFieldSpec::All => 0,
RestFieldSpec::Fields(f) => f.len(),
}
+ embeddings.len()
+ embedding_counts.len();
if total_count > MAX_VARIABLES_COUNT {
return Err(validation_error(format!(
"Too many parameters ({total_count}). Maximum allowed: {MAX_VARIABLES_COUNT}."
)));
}
let embedding_filters = self.extract_embedding_filters(query_pairs)?;
Ok(ExtractedParams {
path_params,
where_clause,
order_by,
pagination,
field_selection,
search_query,
embeddings,
embedding_filters,
embedding_counts,
})
}
fn extract_path_params(
&self,
pairs: &[(&str, &str)],
) -> Result<Vec<(String, serde_json::Value)>, FraiseQLError> {
let mut out = Vec::with_capacity(pairs.len());
for &(name, raw) in pairs {
let arg = self.query_def.arguments.iter().find(|a| a.name == name);
let value = match arg {
Some(a) => coerce_to_type(raw, &a.arg_type)?,
None => serde_json::Value::String(raw.to_string()),
};
out.push((name.to_string(), value));
}
Ok(out)
}
fn parse_select_with_embeddings(
&self,
raw: Option<&str>,
) -> Result<(RestFieldSpec, Vec<EmbeddedSpec>, Vec<String>), FraiseQLError> {
let Some(raw) = raw else {
return Ok((RestFieldSpec::All, Vec::new(), Vec::new()));
};
if raw.is_empty() {
return Ok((RestFieldSpec::All, Vec::new(), Vec::new()));
}
let entries = parse_select_entries(raw)?;
let max_depth = self.config.max_embedding_depth;
let mut flat_fields = Vec::new();
let mut embedded = Vec::new();
let mut counts = Vec::new();
for entry in entries {
match entry {
SelectEntry::Field(name) => {
self.validate_field_name(&name)?;
flat_fields.push(name);
},
SelectEntry::Embedded(spec) => {
validate_embedding_depth(&spec, 1, max_depth as usize)?;
self.validate_embedding_relationship(&spec)?;
embedded.push(spec);
},
SelectEntry::Count(name) => {
self.validate_embedding_relationship_name(&name)?;
counts.push(name);
},
}
}
let field_spec = if flat_fields.is_empty() && !embedded.is_empty() {
RestFieldSpec::All
} else {
RestFieldSpec::Fields(flat_fields)
};
Ok((field_spec, embedded, counts))
}
fn validate_embedding_relationship(&self, spec: &EmbeddedSpec) -> Result<(), FraiseQLError> {
self.validate_embedding_relationship_name(&spec.relationship)
}
fn validate_embedding_relationship_name(&self, name: &str) -> Result<(), FraiseQLError> {
let Some(td) = self.type_def else {
return Err(validation_error(format!(
"Cannot embed '{name}': type definition not available"
)));
};
let has_rel = td.relationships.iter().any(|r| r.name == name);
if !has_rel {
let available: Vec<&str> = td.relationships.iter().map(|r| r.name.as_str()).collect();
let avail_str = if available.is_empty() {
"none".to_string()
} else {
available.join(", ")
};
return Err(validation_error(format!(
"Type '{}' has no relationship '{name}'. Available: {avail_str}",
td.name.as_str()
)));
}
Ok(())
}
fn extract_embedding_filters(
&self,
query_pairs: &[(&str, &str)],
) -> Result<HashMap<String, serde_json::Value>, FraiseQLError> {
let mut filters: HashMap<String, serde_json::Value> = HashMap::new();
for &(key, value) in query_pairs {
if let Some((full_field, op)) = parse_bracket_key(key) {
if let Some(dot_pos) = full_field.find('.') {
let rel_name = &full_field[..dot_pos];
let field_name = &full_field[dot_pos + 1..];
let entry = filters
.entry(rel_name.to_string())
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
if let Some(obj) = entry.as_object_mut() {
obj.insert(field_name.to_string(), serde_json::json!({ op: value }));
}
continue;
}
}
if let Some(dot_pos) = key.find('.') {
if !RESERVED_PARAMS.contains(&key) {
let rel_name = &key[..dot_pos];
let field_name = &key[dot_pos + 1..];
let entry = filters
.entry(rel_name.to_string())
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
if let Some(obj) = entry.as_object_mut() {
obj.insert(field_name.to_string(), serde_json::json!({ "eq": value }));
}
}
}
}
Ok(filters)
}
fn parse_sort(&self, raw: Option<&str>) -> Result<Option<serde_json::Value>, FraiseQLError> {
let Some(raw) = raw else {
return Ok(None);
};
if raw.is_empty() {
return Ok(None);
}
let mut order_parts = Vec::new();
for part in raw.split(',') {
let part = part.trim();
let (field, direction) = if let Some(stripped) = part.strip_prefix('-') {
(stripped, "DESC")
} else {
(part, "ASC")
};
self.validate_field_name(field)?;
order_parts.push(serde_json::json!({
"field": field,
"direction": direction,
}));
}
Ok(Some(serde_json::Value::Array(order_parts)))
}
fn parse_filter(&self, raw: Option<&str>) -> Result<Option<serde_json::Value>, FraiseQLError> {
let Some(raw) = raw else {
return Ok(None);
};
if raw.len() > usize::try_from(self.config.max_filter_bytes).unwrap_or(usize::MAX) {
return Err(validation_error(format!(
"Filter parameter exceeds maximum size ({} bytes). \
Maximum allowed: {} bytes.",
raw.len(),
self.config.max_filter_bytes
)));
}
let parsed: serde_json::Value = serde_json::from_str(raw)
.map_err(|e| validation_error(format!("Invalid filter JSON: {e}")))?;
if json_depth(&parsed) > MAX_FILTER_DEPTH {
return Err(validation_error(format!(
"Filter JSON exceeds maximum nesting depth ({MAX_FILTER_DEPTH})."
)));
}
self.validate_filter_value(&parsed)?;
Ok(Some(parsed))
}
fn validate_filter_value(&self, value: &serde_json::Value) -> Result<(), FraiseQLError> {
let Some(obj) = value.as_object() else {
return Ok(());
};
for (key, inner) in obj {
if matches!(key.as_str(), "_or" | "_and" | "_not") {
if let Some(arr) = inner.as_array() {
for item in arr {
self.validate_filter_value(item)?;
}
}
continue;
}
self.validate_field_name(key)?;
if let Some(ops) = inner.as_object() {
for op_name in ops.keys() {
if !OPERATOR_REGISTRY.contains_key(op_name.as_str()) {
let available: Vec<&str> = {
let mut ops: Vec<&str> = OPERATOR_REGISTRY.keys().copied().collect();
ops.sort_unstable();
ops
};
return Err(validation_error(format!(
"Unknown filter operator '{op_name}'. \
Available operators: {available}",
available = available.join(", ")
)));
}
}
}
}
Ok(())
}
fn merge_where(
&self,
simple: Vec<(String, serde_json::Value)>,
bracket: Vec<(String, String, String)>,
filter: Option<serde_json::Value>,
) -> Result<Option<serde_json::Value>, FraiseQLError> {
if simple.is_empty() && bracket.is_empty() && filter.is_none() {
return Ok(None);
}
let mut merged = serde_json::Map::new();
for (field, value) in simple {
let entry = merged
.entry(field)
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
if let Some(obj) = entry.as_object_mut() {
obj.insert("eq".to_string(), value);
}
}
for (field, op, value) in bracket {
let coerced = self.coerce_field_value(&field, &value)?;
let entry = merged
.entry(field)
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
if let Some(obj) = entry.as_object_mut() {
obj.insert(op, coerced);
}
}
if let Some(filter_val) = filter {
if let Some(filter_obj) = filter_val.as_object() {
for (key, val) in filter_obj {
let entry = merged
.entry(key.clone())
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
if let (Some(existing), Some(new_ops)) =
(entry.as_object_mut(), val.as_object())
{
for (op, v) in new_ops {
existing.insert(op.clone(), v.clone());
}
} else {
merged.insert(key.clone(), val.clone());
}
}
}
}
if merged.is_empty() {
Ok(None)
} else {
Ok(Some(serde_json::Value::Object(merged)))
}
}
fn parse_logical_groups(
&self,
groups: &[(&str, &str)],
) -> Result<Vec<serde_json::Value>, FraiseQLError> {
let mut result = Vec::with_capacity(groups.len());
for &(op, value) in groups {
let dsl_key = format!("_{op}");
let parsed = parse_logical_group(value, &dsl_key, 1)?;
result.push(parsed);
}
Ok(result)
}
fn merge_where_with_logical(
&self,
simple: Vec<(String, serde_json::Value)>,
bracket: Vec<(String, String, String)>,
filter: Option<serde_json::Value>,
logical: Vec<serde_json::Value>,
) -> Result<Option<serde_json::Value>, FraiseQLError> {
let regular = self.merge_where(simple, bracket, filter)?;
if logical.is_empty() {
return Ok(regular);
}
match regular {
Some(regular_where) => {
let mut and_parts = vec![regular_where];
and_parts.extend(logical);
Ok(Some(serde_json::json!({ "_and": and_parts })))
},
None if logical.len() == 1 => {
Ok(Some(logical.into_iter().next().expect("match guard guarantees len == 1")))
},
None => {
Ok(Some(serde_json::json!({ "_and": logical })))
},
}
}
fn parse_offset_pagination(
&self,
limit_raw: Option<&str>,
offset_raw: Option<&str>,
) -> Result<PaginationParams, FraiseQLError> {
let limit = match limit_raw {
Some(s) => {
let v: u64 = s.parse().map_err(|_| {
validation_error(format!(
"Invalid `limit` value: '{s}'. Expected a positive integer."
))
})?;
v.min(self.config.max_page_size)
},
None => self.config.default_page_size,
};
let offset = match offset_raw {
Some(s) => s.parse().map_err(|_| {
validation_error(format!(
"Invalid `offset` value: '{s}'. Expected a non-negative integer."
))
})?,
None => 0,
};
Ok(PaginationParams::Offset { limit, offset })
}
fn parse_cursor_pagination(
&self,
first_raw: Option<&str>,
after_raw: Option<&str>,
last_raw: Option<&str>,
before_raw: Option<&str>,
) -> Result<PaginationParams, FraiseQLError> {
let first = match first_raw {
Some(s) => {
let v: u64 = s.parse().map_err(|_| {
validation_error(format!(
"Invalid `first` value: '{s}'. Expected a positive integer."
))
})?;
Some(v.min(self.config.max_page_size))
},
None if after_raw.is_none() && last_raw.is_none() && before_raw.is_none() => {
Some(self.config.default_page_size)
},
None => None,
};
let after = after_raw.map(String::from);
let last = match last_raw {
Some(s) => Some(s.parse().map_err(|_| {
validation_error(format!(
"Invalid `last` value: '{s}'. Expected a positive integer."
))
})?),
None => None,
};
let before = before_raw.map(String::from);
Ok(PaginationParams::Cursor {
first,
after,
last,
before,
})
}
fn validate_field_name(&self, name: &str) -> Result<(), FraiseQLError> {
if let Some(td) = self.type_def {
if td.find_field_by_output_name(name).is_none() {
let available = field_names(td);
return Err(validation_error(format!(
"Unknown field '{name}'. Available fields: {available}",
available = available.join(", ")
)));
}
}
Ok(())
}
fn validate_bracket_operator(&self, op: &str) -> Result<(), FraiseQLError> {
if !BRACKET_OPERATORS.contains(&op) {
return Err(validation_error(format!(
"Unknown bracket operator '{op}'. \
Available bracket operators: {available}",
available = BRACKET_OPERATORS.join(", ")
)));
}
Ok(())
}
fn is_valid_field(&self, name: &str) -> bool {
match self.type_def {
Some(td) => td.find_field_by_output_name(name).is_some(),
None => true,
}
}
fn coerce_field_value(
&self,
field_name: &str,
raw: &str,
) -> Result<serde_json::Value, FraiseQLError> {
if let Some(td) = self.type_def {
if let Some(fd) = td.find_field_by_output_name(field_name) {
return coerce_to_type(raw, &fd.field_type);
}
}
Ok(serde_json::Value::String(raw.to_string()))
}
fn unknown_param_error(&self, key: &str) -> FraiseQLError {
let mut available: Vec<&str> = RESERVED_PARAMS.to_vec();
if let Some(td) = self.type_def {
for f in &td.fields {
available.push(f.output_name());
}
}
available.sort_unstable();
available.dedup();
validation_error(format!(
"Unknown query parameter '{key}'. Available parameters: {available}",
available = available.join(", ")
))
}
}