1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(Clone, Debug, Serialize, Deserialize)]
10pub struct AppConfig {
11 #[serde(default = "default_db_uri")]
16 pub db_uri: String,
17
18 #[serde(default = "default_db_schemas")]
20 pub db_schemas: Vec<String>,
21
22 pub db_anon_role: Option<String>,
24
25 #[serde(default = "default_pool_size")]
27 pub db_pool_size: u32,
28
29 #[serde(default = "default_pool_timeout")]
31 pub db_pool_timeout: u64,
32
33 #[serde(default = "default_true")]
35 pub db_prepared_statements: bool,
36
37 #[serde(default = "default_extra_search_path")]
45 pub db_extra_search_path: Vec<String>,
46
47 #[serde(default = "default_db_channel")]
49 pub db_channel: String,
50
51 #[serde(default)]
53 pub db_channel_enabled: bool,
54
55 pub db_pre_request: Option<String>,
57
58 pub db_max_rows: Option<i64>,
60
61 #[serde(default)]
66 pub db_aggregates_enabled: bool,
67
68 #[serde(default = "default_host")]
73 pub server_host: String,
74
75 #[serde(default = "default_port")]
77 pub server_port: u16,
78
79 pub server_unix_socket: Option<String>,
81
82 pub admin_server_port: Option<u16>,
84
85 pub jwt_secret: Option<String>,
90
91 #[serde(default)]
93 pub jwt_secret_is_base64: bool,
94
95 pub jwt_aud: Option<String>,
97
98 #[serde(default = "default_jwt_role_claim")]
100 pub jwt_role_claim_key: String,
101
102 #[serde(default = "default_true")]
104 pub jwt_cache_enabled: bool,
105
106 #[serde(default = "default_jwt_cache_max")]
108 pub jwt_cache_max_lifetime: u64,
109
110 pub hasura_admin_secret: Option<String>,
123
124 pub hasura_unauthorized_role: Option<String>,
128
129 pub openapi_server_proxy_uri: Option<String>,
134
135 #[serde(default = "default_openapi_mode")]
137 pub openapi_mode: OpenApiMode,
138
139 #[serde(default = "default_log_level")]
144 pub log_level: LogLevel,
145
146 #[serde(default)]
151 pub role_settings: HashMap<String, RoleSettings>,
152
153 #[serde(default)]
155 pub app_settings: HashMap<String, String>,
156
157 #[serde(default)]
169 pub compat_mode: bool,
170}
171
172impl Default for AppConfig {
173 fn default() -> Self {
174 Self {
175 db_uri: default_db_uri(),
176 db_schemas: default_db_schemas(),
177 db_anon_role: None,
178 db_pool_size: default_pool_size(),
179 db_pool_timeout: default_pool_timeout(),
180 db_prepared_statements: true,
181 db_extra_search_path: default_extra_search_path(),
182 db_channel: default_db_channel(),
183 db_channel_enabled: false,
184 db_pre_request: None,
185 db_max_rows: None,
186 db_aggregates_enabled: false,
187 server_host: default_host(),
188 server_port: default_port(),
189 server_unix_socket: None,
190 admin_server_port: None,
191 jwt_secret: None,
192 jwt_secret_is_base64: false,
193 jwt_aud: None,
194 jwt_role_claim_key: default_jwt_role_claim(),
195 jwt_cache_enabled: true,
196 jwt_cache_max_lifetime: default_jwt_cache_max(),
197 hasura_admin_secret: None,
198 hasura_unauthorized_role: None,
199 openapi_server_proxy_uri: None,
200 openapi_mode: OpenApiMode::FollowPrivileges,
201 log_level: LogLevel::Error,
202 role_settings: HashMap::new(),
203 app_settings: HashMap::new(),
204 compat_mode: false,
205 }
206 }
207}
208
209impl AppConfig {
210 pub fn from_env() -> Self {
212 let mut config = Self::default();
213
214 if let Ok(uri) = std::env::var("PGRST_DB_URI") {
215 config.db_uri = uri;
216 }
217 if let Ok(uri) = std::env::var("DATABASE_URL") {
218 config.db_uri = uri;
219 }
220 if let Ok(schemas) = std::env::var("PGRST_DB_SCHEMAS") {
221 config.db_schemas = schemas.split(',').map(|s| s.trim().to_string()).collect();
222 }
223 if let Ok(role) = std::env::var("PGRST_DB_ANON_ROLE") {
224 config.db_anon_role = Some(role);
225 }
226 if let Ok(size) = std::env::var("PGRST_DB_POOL") {
227 if let Ok(n) = size.parse() {
228 config.db_pool_size = n;
229 }
230 }
231 if let Ok(v) = std::env::var("PGRST_DB_EXTRA_SEARCH_PATH") {
233 config.db_extra_search_path = v
234 .split(',')
235 .map(|s| s.trim().to_string())
236 .filter(|s| !s.is_empty())
237 .collect();
238 }
239
240 if let Ok(v) = std::env::var("PGRST_DB_AGGREGATES_ENABLED") {
241 config.db_aggregates_enabled = matches!(v.to_ascii_lowercase().as_str(), "1" | "true");
242 }
243
244 for var in ["PGRST_DB_MAX_ROWS", "PGRST_MAX_ROWS"] {
246 if let Ok(max_rows) = std::env::var(var) {
247 match max_rows.parse::<i64>() {
248 Ok(n) if n >= 0 => config.db_max_rows = Some(n),
249 _ => {
250 tracing::warn!(
251 "Ignoring {}={:?}: expected a non-negative integer",
252 var,
253 max_rows
254 );
255 }
256 }
257 }
258 }
259 if let Ok(secret) = std::env::var("PGRST_JWT_SECRET") {
260 config.jwt_secret = Some(secret);
261 }
262 if let Ok(aud) = std::env::var("PGRST_JWT_AUD") {
263 config.jwt_aud = Some(aud);
264 }
265 for var in ["HASURA_GRAPHQL_ADMIN_SECRET", "PGRST_HASURA_ADMIN_SECRET"] {
271 if let Ok(secret) = std::env::var(var) {
272 if !secret.is_empty() {
273 config.hasura_admin_secret = Some(secret);
274 }
275 }
276 }
277 for var in [
278 "HASURA_GRAPHQL_UNAUTHORIZED_ROLE",
279 "PGRST_HASURA_UNAUTHORIZED_ROLE",
280 ] {
281 if let Ok(role) = std::env::var(var) {
282 if !role.is_empty() {
283 config.hasura_unauthorized_role = Some(role);
284 }
285 }
286 }
287 if let Ok(host) = std::env::var("PGRST_SERVER_HOST") {
288 config.server_host = host;
289 }
290 if let Ok(port) = std::env::var("PGRST_SERVER_PORT") {
291 if let Ok(p) = port.parse() {
292 config.server_port = p;
293 }
294 }
295 if let Ok(port) = std::env::var("PORT") {
296 if let Ok(p) = port.parse() {
297 config.server_port = p;
298 }
299 }
300 for var in ["PGRST_COMPAT_MODE", "POSTRUST_COMPAT_MODE"] {
303 if let Ok(v) = std::env::var(var) {
304 config.compat_mode = matches!(
305 v.trim().to_ascii_lowercase().as_str(),
306 "true" | "1" | "yes" | "on"
307 );
308 }
309 }
310
311 config
312 }
313
314 pub fn default_schema(&self) -> &str {
316 self.db_schemas
317 .first()
318 .map(|s| s.as_str())
319 .unwrap_or("public")
320 }
321}
322
323#[derive(Clone, Debug, Serialize, Deserialize)]
325pub struct RoleSettings {
326 pub isolation_level: Option<IsolationLevel>,
328 pub statement_timeout: Option<u64>,
330}
331
332#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
334pub enum IsolationLevel {
335 ReadCommitted,
336 RepeatableRead,
337 Serializable,
338}
339
340impl IsolationLevel {
341 pub fn to_sql(&self) -> &'static str {
342 match self {
343 Self::ReadCommitted => "READ COMMITTED",
344 Self::RepeatableRead => "REPEATABLE READ",
345 Self::Serializable => "SERIALIZABLE",
346 }
347 }
348}
349
350#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
352pub enum OpenApiMode {
353 Disabled,
354 FollowPrivileges,
355 IgnorePrivileges,
356 SecurityDefiner,
357}
358
359#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
361pub enum LogLevel {
362 Crit,
363 Error,
364 Warn,
365 Info,
366 Debug,
367}
368
369impl LogLevel {
370 pub fn to_tracing(&self) -> tracing::Level {
371 match self {
372 Self::Crit | Self::Error => tracing::Level::ERROR,
373 Self::Warn => tracing::Level::WARN,
374 Self::Info => tracing::Level::INFO,
375 Self::Debug => tracing::Level::DEBUG,
376 }
377 }
378}
379
380fn default_db_uri() -> String {
382 "postgresql://localhost/postgres".to_string()
383}
384
385fn default_db_schemas() -> Vec<String> {
386 vec!["public".to_string()]
387}
388
389fn default_pool_size() -> u32 {
390 10
391}
392
393fn default_pool_timeout() -> u64 {
394 10
395}
396
397fn default_extra_search_path() -> Vec<String> {
398 vec!["public".to_string()]
399}
400
401fn default_db_channel() -> String {
402 "pgrst".to_string()
403}
404
405fn default_host() -> String {
406 "127.0.0.1".to_string()
407}
408
409fn default_port() -> u16 {
410 3000
411}
412
413fn default_jwt_role_claim() -> String {
414 "role".to_string()
415}
416
417fn default_jwt_cache_max() -> u64 {
418 3600
419}
420
421fn default_openapi_mode() -> OpenApiMode {
422 OpenApiMode::FollowPrivileges
423}
424
425fn default_log_level() -> LogLevel {
426 LogLevel::Error
427}
428
429fn default_true() -> bool {
430 true
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn test_default_config() {
439 let config = AppConfig::default();
440 assert_eq!(config.server_port, 3000);
441 assert_eq!(config.db_pool_size, 10);
442 assert!(config.db_prepared_statements);
443 }
444
445 #[test]
446 fn test_default_schema() {
447 let mut config = AppConfig::default();
448 assert_eq!(config.default_schema(), "public");
449
450 config.db_schemas = vec!["api".to_string(), "public".to_string()];
451 assert_eq!(config.default_schema(), "api");
452 }
453
454 #[test]
455 fn test_isolation_level_sql() {
456 assert_eq!(IsolationLevel::ReadCommitted.to_sql(), "READ COMMITTED");
457 assert_eq!(IsolationLevel::Serializable.to_sql(), "SERIALIZABLE");
458 }
459}