Skip to main content

postrust_core/
config.rs

1//! Configuration for Postrust.
2//!
3//! Mirrors PostgREST's configuration options.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// Main application configuration.
9#[derive(Clone, Debug, Serialize, Deserialize)]
10pub struct AppConfig {
11    // ========================================================================
12    // Database Settings
13    // ========================================================================
14    /// PostgreSQL connection URI
15    #[serde(default = "default_db_uri")]
16    pub db_uri: String,
17
18    /// Schemas to expose via the API
19    #[serde(default = "default_db_schemas")]
20    pub db_schemas: Vec<String>,
21
22    /// Role for unauthenticated requests
23    pub db_anon_role: Option<String>,
24
25    /// Connection pool size
26    #[serde(default = "default_pool_size")]
27    pub db_pool_size: u32,
28
29    /// Pool acquisition timeout in seconds
30    #[serde(default = "default_pool_timeout")]
31    pub db_pool_timeout: u64,
32
33    /// Use prepared statements
34    #[serde(default = "default_true")]
35    pub db_prepared_statements: bool,
36
37    /// Extra search path schemas
38    #[serde(default)]
39    pub db_extra_search_path: Vec<String>,
40
41    /// LISTEN/NOTIFY channel for schema reload
42    #[serde(default = "default_db_channel")]
43    pub db_channel: String,
44
45    /// Enable NOTIFY-based schema cache reload
46    #[serde(default)]
47    pub db_channel_enabled: bool,
48
49    /// Pre-request function to call
50    pub db_pre_request: Option<String>,
51
52    /// Maximum rows allowed in a response
53    pub db_max_rows: Option<i64>,
54
55    /// Enable aggregate functions
56    #[serde(default = "default_true")]
57    pub db_aggregates_enabled: bool,
58
59    // ========================================================================
60    // Server Settings
61    // ========================================================================
62    /// Server host to bind
63    #[serde(default = "default_host")]
64    pub server_host: String,
65
66    /// Server port
67    #[serde(default = "default_port")]
68    pub server_port: u16,
69
70    /// Unix socket path (alternative to host/port)
71    pub server_unix_socket: Option<String>,
72
73    /// Admin server port (for health checks)
74    pub admin_server_port: Option<u16>,
75
76    // ========================================================================
77    // JWT Settings
78    // ========================================================================
79    /// JWT secret key (or JWKS URL)
80    pub jwt_secret: Option<String>,
81
82    /// JWT secret as base64
83    #[serde(default)]
84    pub jwt_secret_is_base64: bool,
85
86    /// JWT audience claim to validate
87    pub jwt_aud: Option<String>,
88
89    /// JWT claim that contains the role
90    #[serde(default = "default_jwt_role_claim")]
91    pub jwt_role_claim_key: String,
92
93    /// Cache JWT validations
94    #[serde(default = "default_true")]
95    pub jwt_cache_enabled: bool,
96
97    /// JWT cache max entries
98    #[serde(default = "default_jwt_cache_max")]
99    pub jwt_cache_max_lifetime: u64,
100
101    // ========================================================================
102    // OpenAPI Settings
103    // ========================================================================
104    /// OpenAPI server URL
105    pub openapi_server_proxy_uri: Option<String>,
106
107    /// OpenAPI mode: disabled, follow-privileges, ignore-privileges, security-definer
108    #[serde(default = "default_openapi_mode")]
109    pub openapi_mode: OpenApiMode,
110
111    // ========================================================================
112    // Logging Settings
113    // ========================================================================
114    /// Log level: crit, error, warn, info, debug
115    #[serde(default = "default_log_level")]
116    pub log_level: LogLevel,
117
118    // ========================================================================
119    // Role Settings
120    // ========================================================================
121    /// Per-role settings (isolation level, timeout)
122    #[serde(default)]
123    pub role_settings: HashMap<String, RoleSettings>,
124
125    /// App-level settings to expose via GUC
126    #[serde(default)]
127    pub app_settings: HashMap<String, String>,
128
129    // ========================================================================
130    // Compatibility Settings
131    // ========================================================================
132    /// PostgREST compatibility mode.
133    ///
134    /// When enabled, the REST surface is also served at the root (so canonical
135    /// PostgREST paths like `/rpc/<name>` and `/<table>` work in addition to
136    /// the `/api`-prefixed paths), and RPC responses are un-wrapped to match
137    /// PostgREST's shape (bare object/scalar for non-set-returning functions,
138    /// a top-level array for set-returning ones) instead of the array-wrapped,
139    /// function-name-keyed default.
140    #[serde(default)]
141    pub compat_mode: bool,
142}
143
144impl Default for AppConfig {
145    fn default() -> Self {
146        Self {
147            db_uri: default_db_uri(),
148            db_schemas: default_db_schemas(),
149            db_anon_role: None,
150            db_pool_size: default_pool_size(),
151            db_pool_timeout: default_pool_timeout(),
152            db_prepared_statements: true,
153            db_extra_search_path: vec![],
154            db_channel: default_db_channel(),
155            db_channel_enabled: false,
156            db_pre_request: None,
157            db_max_rows: None,
158            db_aggregates_enabled: true,
159            server_host: default_host(),
160            server_port: default_port(),
161            server_unix_socket: None,
162            admin_server_port: None,
163            jwt_secret: None,
164            jwt_secret_is_base64: false,
165            jwt_aud: None,
166            jwt_role_claim_key: default_jwt_role_claim(),
167            jwt_cache_enabled: true,
168            jwt_cache_max_lifetime: default_jwt_cache_max(),
169            openapi_server_proxy_uri: None,
170            openapi_mode: OpenApiMode::FollowPrivileges,
171            log_level: LogLevel::Error,
172            role_settings: HashMap::new(),
173            app_settings: HashMap::new(),
174            compat_mode: false,
175        }
176    }
177}
178
179impl AppConfig {
180    /// Load configuration from environment variables.
181    pub fn from_env() -> Self {
182        let mut config = Self::default();
183
184        if let Ok(uri) = std::env::var("PGRST_DB_URI") {
185            config.db_uri = uri;
186        }
187        if let Ok(uri) = std::env::var("DATABASE_URL") {
188            config.db_uri = uri;
189        }
190        if let Ok(schemas) = std::env::var("PGRST_DB_SCHEMAS") {
191            config.db_schemas = schemas.split(',').map(|s| s.trim().to_string()).collect();
192        }
193        if let Ok(role) = std::env::var("PGRST_DB_ANON_ROLE") {
194            config.db_anon_role = Some(role);
195        }
196        if let Ok(size) = std::env::var("PGRST_DB_POOL") {
197            if let Ok(n) = size.parse() {
198                config.db_pool_size = n;
199            }
200        }
201        if let Ok(secret) = std::env::var("PGRST_JWT_SECRET") {
202            config.jwt_secret = Some(secret);
203        }
204        if let Ok(aud) = std::env::var("PGRST_JWT_AUD") {
205            config.jwt_aud = Some(aud);
206        }
207        if let Ok(host) = std::env::var("PGRST_SERVER_HOST") {
208            config.server_host = host;
209        }
210        if let Ok(port) = std::env::var("PGRST_SERVER_PORT") {
211            if let Ok(p) = port.parse() {
212                config.server_port = p;
213            }
214        }
215        if let Ok(port) = std::env::var("PORT") {
216            if let Ok(p) = port.parse() {
217                config.server_port = p;
218            }
219        }
220        // Accept either the PGRST_-prefixed name (for parity with other options)
221        // or a POSTRUST_-prefixed alias.
222        for var in ["PGRST_COMPAT_MODE", "POSTRUST_COMPAT_MODE"] {
223            if let Ok(v) = std::env::var(var) {
224                config.compat_mode = matches!(
225                    v.trim().to_ascii_lowercase().as_str(),
226                    "true" | "1" | "yes" | "on"
227                );
228            }
229        }
230
231        config
232    }
233
234    /// Get the default schema (first in the list).
235    pub fn default_schema(&self) -> &str {
236        self.db_schemas
237            .first()
238            .map(|s| s.as_str())
239            .unwrap_or("public")
240    }
241}
242
243/// Per-role settings.
244#[derive(Clone, Debug, Serialize, Deserialize)]
245pub struct RoleSettings {
246    /// Isolation level for this role
247    pub isolation_level: Option<IsolationLevel>,
248    /// Statement timeout in milliseconds
249    pub statement_timeout: Option<u64>,
250}
251
252/// Transaction isolation levels.
253#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
254pub enum IsolationLevel {
255    ReadCommitted,
256    RepeatableRead,
257    Serializable,
258}
259
260impl IsolationLevel {
261    pub fn to_sql(&self) -> &'static str {
262        match self {
263            Self::ReadCommitted => "READ COMMITTED",
264            Self::RepeatableRead => "REPEATABLE READ",
265            Self::Serializable => "SERIALIZABLE",
266        }
267    }
268}
269
270/// OpenAPI generation mode.
271#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
272pub enum OpenApiMode {
273    Disabled,
274    FollowPrivileges,
275    IgnorePrivileges,
276    SecurityDefiner,
277}
278
279/// Log levels.
280#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
281pub enum LogLevel {
282    Crit,
283    Error,
284    Warn,
285    Info,
286    Debug,
287}
288
289impl LogLevel {
290    pub fn to_tracing(&self) -> tracing::Level {
291        match self {
292            Self::Crit | Self::Error => tracing::Level::ERROR,
293            Self::Warn => tracing::Level::WARN,
294            Self::Info => tracing::Level::INFO,
295            Self::Debug => tracing::Level::DEBUG,
296        }
297    }
298}
299
300// Default value functions
301fn default_db_uri() -> String {
302    "postgresql://localhost/postgres".to_string()
303}
304
305fn default_db_schemas() -> Vec<String> {
306    vec!["public".to_string()]
307}
308
309fn default_pool_size() -> u32 {
310    10
311}
312
313fn default_pool_timeout() -> u64 {
314    10
315}
316
317fn default_db_channel() -> String {
318    "pgrst".to_string()
319}
320
321fn default_host() -> String {
322    "127.0.0.1".to_string()
323}
324
325fn default_port() -> u16 {
326    3000
327}
328
329fn default_jwt_role_claim() -> String {
330    "role".to_string()
331}
332
333fn default_jwt_cache_max() -> u64 {
334    3600
335}
336
337fn default_openapi_mode() -> OpenApiMode {
338    OpenApiMode::FollowPrivileges
339}
340
341fn default_log_level() -> LogLevel {
342    LogLevel::Error
343}
344
345fn default_true() -> bool {
346    true
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    #[test]
354    fn test_default_config() {
355        let config = AppConfig::default();
356        assert_eq!(config.server_port, 3000);
357        assert_eq!(config.db_pool_size, 10);
358        assert!(config.db_prepared_statements);
359    }
360
361    #[test]
362    fn test_default_schema() {
363        let mut config = AppConfig::default();
364        assert_eq!(config.default_schema(), "public");
365
366        config.db_schemas = vec!["api".to_string(), "public".to_string()];
367        assert_eq!(config.default_schema(), "api");
368    }
369
370    #[test]
371    fn test_isolation_level_sql() {
372        assert_eq!(IsolationLevel::ReadCommitted.to_sql(), "READ COMMITTED");
373        assert_eq!(IsolationLevel::Serializable.to_sql(), "SERIALIZABLE");
374    }
375}