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        // `PGRST_MAX_ROWS` is the name used in our own documentation;
202        // `PGRST_DB_MAX_ROWS` mirrors PostgREST's `db-max-rows`. Accept both.
203        for var in ["PGRST_DB_MAX_ROWS", "PGRST_MAX_ROWS"] {
204            if let Ok(max_rows) = std::env::var(var) {
205                match max_rows.parse::<i64>() {
206                    Ok(n) if n >= 0 => config.db_max_rows = Some(n),
207                    _ => {
208                        tracing::warn!(
209                            "Ignoring {}={:?}: expected a non-negative integer",
210                            var,
211                            max_rows
212                        );
213                    }
214                }
215            }
216        }
217        if let Ok(secret) = std::env::var("PGRST_JWT_SECRET") {
218            config.jwt_secret = Some(secret);
219        }
220        if let Ok(aud) = std::env::var("PGRST_JWT_AUD") {
221            config.jwt_aud = Some(aud);
222        }
223        if let Ok(host) = std::env::var("PGRST_SERVER_HOST") {
224            config.server_host = host;
225        }
226        if let Ok(port) = std::env::var("PGRST_SERVER_PORT") {
227            if let Ok(p) = port.parse() {
228                config.server_port = p;
229            }
230        }
231        if let Ok(port) = std::env::var("PORT") {
232            if let Ok(p) = port.parse() {
233                config.server_port = p;
234            }
235        }
236        // Accept either the PGRST_-prefixed name (for parity with other options)
237        // or a POSTRUST_-prefixed alias.
238        for var in ["PGRST_COMPAT_MODE", "POSTRUST_COMPAT_MODE"] {
239            if let Ok(v) = std::env::var(var) {
240                config.compat_mode = matches!(
241                    v.trim().to_ascii_lowercase().as_str(),
242                    "true" | "1" | "yes" | "on"
243                );
244            }
245        }
246
247        config
248    }
249
250    /// Get the default schema (first in the list).
251    pub fn default_schema(&self) -> &str {
252        self.db_schemas
253            .first()
254            .map(|s| s.as_str())
255            .unwrap_or("public")
256    }
257}
258
259/// Per-role settings.
260#[derive(Clone, Debug, Serialize, Deserialize)]
261pub struct RoleSettings {
262    /// Isolation level for this role
263    pub isolation_level: Option<IsolationLevel>,
264    /// Statement timeout in milliseconds
265    pub statement_timeout: Option<u64>,
266}
267
268/// Transaction isolation levels.
269#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
270pub enum IsolationLevel {
271    ReadCommitted,
272    RepeatableRead,
273    Serializable,
274}
275
276impl IsolationLevel {
277    pub fn to_sql(&self) -> &'static str {
278        match self {
279            Self::ReadCommitted => "READ COMMITTED",
280            Self::RepeatableRead => "REPEATABLE READ",
281            Self::Serializable => "SERIALIZABLE",
282        }
283    }
284}
285
286/// OpenAPI generation mode.
287#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
288pub enum OpenApiMode {
289    Disabled,
290    FollowPrivileges,
291    IgnorePrivileges,
292    SecurityDefiner,
293}
294
295/// Log levels.
296#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
297pub enum LogLevel {
298    Crit,
299    Error,
300    Warn,
301    Info,
302    Debug,
303}
304
305impl LogLevel {
306    pub fn to_tracing(&self) -> tracing::Level {
307        match self {
308            Self::Crit | Self::Error => tracing::Level::ERROR,
309            Self::Warn => tracing::Level::WARN,
310            Self::Info => tracing::Level::INFO,
311            Self::Debug => tracing::Level::DEBUG,
312        }
313    }
314}
315
316// Default value functions
317fn default_db_uri() -> String {
318    "postgresql://localhost/postgres".to_string()
319}
320
321fn default_db_schemas() -> Vec<String> {
322    vec!["public".to_string()]
323}
324
325fn default_pool_size() -> u32 {
326    10
327}
328
329fn default_pool_timeout() -> u64 {
330    10
331}
332
333fn default_db_channel() -> String {
334    "pgrst".to_string()
335}
336
337fn default_host() -> String {
338    "127.0.0.1".to_string()
339}
340
341fn default_port() -> u16 {
342    3000
343}
344
345fn default_jwt_role_claim() -> String {
346    "role".to_string()
347}
348
349fn default_jwt_cache_max() -> u64 {
350    3600
351}
352
353fn default_openapi_mode() -> OpenApiMode {
354    OpenApiMode::FollowPrivileges
355}
356
357fn default_log_level() -> LogLevel {
358    LogLevel::Error
359}
360
361fn default_true() -> bool {
362    true
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    #[test]
370    fn test_default_config() {
371        let config = AppConfig::default();
372        assert_eq!(config.server_port, 3000);
373        assert_eq!(config.db_pool_size, 10);
374        assert!(config.db_prepared_statements);
375    }
376
377    #[test]
378    fn test_default_schema() {
379        let mut config = AppConfig::default();
380        assert_eq!(config.default_schema(), "public");
381
382        config.db_schemas = vec!["api".to_string(), "public".to_string()];
383        assert_eq!(config.default_schema(), "api");
384    }
385
386    #[test]
387    fn test_isolation_level_sql() {
388        assert_eq!(IsolationLevel::ReadCommitted.to_sql(), "READ COMMITTED");
389        assert_eq!(IsolationLevel::Serializable.to_sql(), "SERIALIZABLE");
390    }
391}