use fraiseql_core::schema::{DeleteResponse, RestConfig};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct RestTomlConfig {
pub enabled: bool,
pub path: String,
pub max_page_size: u64,
pub default_page_size: u64,
pub default_embed_page_size: u64,
pub ndjson_batch_size: u64,
pub max_bulk_affected: u64,
pub max_filter_bytes: u64,
pub delete_response: DeleteResponseToml,
pub default_cache_ttl: u64,
pub cdn_max_age: Option<u64>,
pub require_auth: bool,
pub sse_heartbeat_seconds: u64,
pub sse_max_replay_events: u64,
pub max_embedding_depth: u32,
pub include: Vec<String>,
pub exclude: Vec<String>,
pub etag: bool,
pub idempotency_ttl_seconds: u64,
#[serde(skip_serializing)]
pub max_embedded_reads: Option<serde::de::IgnoredAny>,
}
impl RestTomlConfig {
pub fn reject_retired_keys(&self) -> anyhow::Result<()> {
if self.max_embedded_reads.is_some() {
anyhow::bail!(
"[rest] max_embedded_reads was removed: a `?select=` embed is no longer a \
sub-read per parent row but one composed statement, and there are no sub-reads \
left to count. It is bounded by the request's cost ceiling — set \
[security.cost_budget] per_request_max (and [validation] max_response_bytes \
for its bytes) — and each embedded level's page by [rest] \
default_embed_page_size. Remove the key."
);
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DeleteResponseToml {
#[default]
NoContent,
Entity,
}
impl Default for RestTomlConfig {
fn default() -> Self {
Self {
enabled: false,
path: "/rest/v1".to_string(),
max_page_size: 1_000,
default_page_size: 100,
default_embed_page_size: 50,
ndjson_batch_size: 500,
max_bulk_affected: 10_000,
max_filter_bytes: 4_096,
delete_response: DeleteResponseToml::NoContent,
default_cache_ttl: 0,
cdn_max_age: None,
require_auth: false,
sse_heartbeat_seconds: 30,
sse_max_replay_events: 10_000,
max_embedding_depth: 3,
include: Vec::new(),
exclude: Vec::new(),
etag: true,
idempotency_ttl_seconds: 300,
max_embedded_reads: None,
}
}
}
impl From<DeleteResponseToml> for DeleteResponse {
fn from(toml: DeleteResponseToml) -> Self {
match toml {
DeleteResponseToml::NoContent => Self::NoContent,
DeleteResponseToml::Entity => Self::Entity,
}
}
}
impl From<RestTomlConfig> for RestConfig {
fn from(toml: RestTomlConfig) -> Self {
Self {
enabled: toml.enabled,
path: toml.path,
max_page_size: toml.max_page_size,
default_page_size: toml.default_page_size,
default_embed_page_size: toml.default_embed_page_size,
ndjson_batch_size: toml.ndjson_batch_size,
max_bulk_affected: toml.max_bulk_affected,
max_filter_bytes: toml.max_filter_bytes,
delete_response: toml.delete_response.into(),
default_cache_ttl: toml.default_cache_ttl,
cdn_max_age: toml.cdn_max_age,
require_auth: toml.require_auth,
sse_heartbeat_seconds: toml.sse_heartbeat_seconds,
sse_max_replay_events: toml.sse_max_replay_events,
max_embedding_depth: toml.max_embedding_depth,
include: toml.include,
exclude: toml.exclude,
etag: toml.etag,
idempotency_ttl_seconds: toml.idempotency_ttl_seconds,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests;