1use base64::Engine;
2use sha2::Digest;
3use sqlx::postgres::PgPoolOptions;
4use std::{collections::HashMap, fmt::Display, pin::Pin, sync::Arc};
5use tokio::sync::Mutex;
6
7pub static BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new(
8 &base64::alphabet::STANDARD,
9 base64::engine::GeneralPurposeConfig::new()
10 .with_decode_allow_trailing_bits(true)
11 .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent),
12);
13
14type BatchFuture = Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + Send>>;
15
16pub struct Database {
17 pub cache: Arc<crate::cache::Cache>,
18
19 write: sqlx::PgPool,
20 read: Option<sqlx::PgPool>,
21
22 encryption_key: Arc<str>,
23 decryption_cache: Option<moka::future::Cache<[u8; 32], Arc<Vec<u8>>>>,
24 batch_actions: Arc<Mutex<HashMap<(&'static str, uuid::Uuid), BatchFuture>>>,
25}
26
27async fn connect_pool(label: &str, url: &str, max_connections: u32) -> sqlx::PgPool {
28 crate::retry::startup_connect(label, || {
29 PgPoolOptions::new()
30 .min_connections(10)
31 .max_connections(max_connections)
32 .test_before_acquire(false)
33 .connect(url)
34 })
35 .await
36}
37
38impl Database {
39 pub async fn new(env: &crate::env::Env, cache: Arc<crate::cache::Cache>) -> Self {
40 let start = std::time::Instant::now();
41
42 let instance = Self {
43 cache,
44
45 write: match &env.database_url_primary {
46 Some(url) => connect_pool("primary database", url, 20).await,
47 None => connect_pool("database", &env.database_url, 50).await,
48 },
49 read: if env.database_url_primary.is_some() {
50 Some(connect_pool("read database", &env.database_url, 50).await)
51 } else {
52 None
53 },
54
55 encryption_key: env.app_encryption_key.clone().into(),
56 decryption_cache: env.app_use_decryption_cache.then(|| {
57 moka::future::Cache::builder()
58 .max_capacity(16384)
59 .time_to_live(std::time::Duration::from_secs(30))
60 .build()
61 }),
62 batch_actions: Arc::new(Mutex::new(HashMap::new())),
63 };
64
65 let version = instance
66 .version()
67 .await
68 .unwrap_or_else(|_| "unknown".into());
69
70 tracing::info!(
71 "database connected (postgres@{}, {}ms)",
72 version,
73 start.elapsed().as_millis()
74 );
75
76 tokio::spawn({
77 let batch_actions = instance.batch_actions.clone();
78
79 async move {
80 let mut interval = tokio::time::interval(std::time::Duration::from_secs(5));
81 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
82
83 loop {
84 interval.tick().await;
85
86 let actions = batch_actions.lock().await.drain().collect::<Vec<_>>();
87
88 for (key, action) in actions {
89 tracing::debug!("executing batch action for {}:{}", key.0, key.1);
90 if let Err(err) = action.await {
91 tracing::error!(
92 "error executing batch action for {}:{} - {:?}",
93 key.0,
94 key.1,
95 err
96 );
97 sentry_anyhow::capture_anyhow(&err);
98 }
99 }
100 }
101 }
102 });
103
104 instance
105 }
106
107 pub async fn flush_batch_actions(&self) {
108 let actions = self.batch_actions.lock().await.drain().collect::<Vec<_>>();
109
110 for (key, action) in actions {
111 tracing::debug!("executing batch action for {}:{}", key.0, key.1);
112 if let Err(err) = action.await {
113 tracing::error!(
114 "error executing batch action for {}:{} - {:?}",
115 key.0,
116 key.1,
117 err
118 );
119 sentry_anyhow::capture_anyhow(&err);
120 }
121 }
122 }
123
124 pub async fn version(&self) -> Result<compact_str::CompactString, sqlx::Error> {
125 let version: (compact_str::CompactString,) =
126 sqlx::query_as("SELECT split_part(version(), ' ', 2)")
127 .fetch_one(self.read())
128 .await?;
129
130 Ok(version.0)
131 }
132
133 pub async fn size(&self) -> Result<u64, sqlx::Error> {
134 let size: (i64,) = sqlx::query_as("SELECT pg_database_size(current_database())")
135 .fetch_one(self.read())
136 .await?;
137
138 Ok(size.0 as u64)
139 }
140
141 #[inline]
142 pub fn write(&self) -> &sqlx::PgPool {
143 &self.write
144 }
145
146 #[inline]
147 pub fn read(&self) -> &sqlx::PgPool {
148 self.read.as_ref().unwrap_or(&self.write)
149 }
150
151 pub async fn encrypt(
152 &self,
153 data: impl AsRef<[u8]> + Send + 'static,
154 ) -> Result<Vec<u8>, anyhow::Error> {
155 let encryption_key = self.encryption_key.clone();
156
157 tokio::task::spawn_blocking(move || {
158 simple_crypt::encrypt(data.as_ref(), encryption_key.as_bytes())
159 })
160 .await?
161 }
162
163 pub async fn encrypt_with_input<P: AsRef<[u8]> + Send + 'static>(
164 &self,
165 data: P,
166 ) -> Result<(P, Vec<u8>), anyhow::Error> {
167 let encryption_key = self.encryption_key.clone();
168
169 tokio::task::spawn_blocking(move || {
170 simple_crypt::encrypt(data.as_ref(), encryption_key.as_bytes())
171 .map(|bytes| (data, bytes))
172 })
173 .await?
174 }
175
176 pub async fn encrypt_base64(
177 &self,
178 data: impl AsRef<[u8]> + Send + 'static,
179 ) -> Result<compact_str::CompactString, anyhow::Error> {
180 let encrypted = self.encrypt(data).await?;
181 Ok(BASE64_ENGINE.encode(&encrypted).into())
182 }
183
184 #[inline]
185 pub fn blocking_encrypt(&self, data: impl AsRef<[u8]>) -> Result<Vec<u8>, anyhow::Error> {
186 simple_crypt::encrypt(data.as_ref(), self.encryption_key.as_bytes())
187 }
188
189 #[inline]
190 pub fn blocking_encrypt_base64(
191 &self,
192 data: impl AsRef<[u8]>,
193 ) -> Result<compact_str::CompactString, anyhow::Error> {
194 let encrypted = self.blocking_encrypt(data)?;
195 Ok(BASE64_ENGINE.encode(&encrypted).into())
196 }
197
198 pub async fn decrypt(
199 &self,
200 data: impl AsRef<[u8]> + Send + 'static,
201 ) -> Result<compact_str::CompactString, anyhow::Error> {
202 let decrypted = self.decrypt_raw(data).await?;
203
204 Ok(compact_str::CompactString::from_utf8_lossy(&decrypted))
205 }
206
207 pub async fn decrypt_raw(
208 &self,
209 data: impl AsRef<[u8]> + Send + 'static,
210 ) -> Result<Vec<u8>, anyhow::Error> {
211 let Some(decryption_cache) = &self.decryption_cache else {
212 return self.decrypt_uncached(data.as_ref().to_vec()).await;
213 };
214
215 let digest: [u8; 32] = sha2::Sha256::digest(data.as_ref()).into();
216 let decrypted = decryption_cache
217 .try_get_with(digest, async {
218 self.decrypt_uncached(data.as_ref().to_vec())
219 .await
220 .map(Arc::new)
221 })
222 .await
223 .map_err(crate::cache::SharedComputeError)?;
224
225 Ok(decrypted.as_ref().clone())
226 }
227
228 async fn decrypt_uncached(&self, data: Vec<u8>) -> Result<Vec<u8>, anyhow::Error> {
229 let encryption_key = self.encryption_key.clone();
230
231 tokio::task::spawn_blocking(move || simple_crypt::decrypt(&data, encryption_key.as_bytes()))
232 .await?
233 }
234
235 pub async fn decrypt_base64(
236 &self,
237 data: impl AsRef<str>,
238 ) -> Result<compact_str::CompactString, anyhow::Error> {
239 let decoded = BASE64_ENGINE.decode(data.as_ref())?;
240 self.decrypt(decoded).await
241 }
242
243 pub async fn decrypt_base64_raw(
244 &self,
245 data: impl AsRef<str>,
246 ) -> Result<Vec<u8>, anyhow::Error> {
247 let decoded = BASE64_ENGINE.decode(data.as_ref())?;
248 self.decrypt_raw(decoded).await
249 }
250
251 pub async fn decrypt_base64_optional(
252 &self,
253 data: impl AsRef<str>,
254 ) -> Result<Option<compact_str::CompactString>, anyhow::Error> {
255 match BASE64_ENGINE.decode(data.as_ref()) {
256 Ok(decoded) => Ok(Some(self.decrypt(decoded).await?)),
257 Err(_) => Ok(None),
258 }
259 }
260
261 pub async fn decrypt_base64_raw_optional(
262 &self,
263 data: impl AsRef<str>,
264 ) -> Result<Option<Vec<u8>>, anyhow::Error> {
265 match BASE64_ENGINE.decode(data.as_ref()) {
266 Ok(decoded) => Ok(Some(self.decrypt_raw(decoded).await?)),
267 Err(_) => Ok(None),
268 }
269 }
270
271 #[inline]
272 pub fn blocking_decrypt(
273 &self,
274 data: impl AsRef<[u8]>,
275 ) -> Result<compact_str::CompactString, anyhow::Error> {
276 simple_crypt::decrypt(data.as_ref(), self.encryption_key.as_bytes())
277 .map(|s| compact_str::CompactString::from_utf8_lossy(&s))
278 }
279
280 #[inline]
281 pub fn blocking_decrypt_raw(&self, data: impl AsRef<[u8]>) -> Result<Vec<u8>, anyhow::Error> {
282 simple_crypt::decrypt(data.as_ref(), self.encryption_key.as_bytes())
283 }
284
285 #[inline]
286 pub fn blocking_decrypt_base64(
287 &self,
288 data: impl AsRef<str>,
289 ) -> Result<compact_str::CompactString, anyhow::Error> {
290 let decoded = BASE64_ENGINE.decode(data.as_ref())?;
291 self.blocking_decrypt(decoded)
292 }
293
294 #[inline]
295 pub fn blocking_decrypt_base64_raw(
296 &self,
297 data: impl AsRef<str>,
298 ) -> Result<Vec<u8>, anyhow::Error> {
299 let decoded = BASE64_ENGINE.decode(data.as_ref())?;
300 self.blocking_decrypt_raw(decoded)
301 }
302
303 #[inline]
304 pub fn blocking_decrypt_base64_optional(
305 &self,
306 data: impl AsRef<str>,
307 ) -> Result<Option<compact_str::CompactString>, anyhow::Error> {
308 match BASE64_ENGINE.decode(data.as_ref()) {
309 Ok(decoded) => Ok(Some(self.blocking_decrypt(decoded)?)),
310 Err(_) => Ok(None),
311 }
312 }
313
314 #[inline]
315 pub fn blocking_decrypt_base64_raw_optional(
316 &self,
317 data: impl AsRef<str>,
318 ) -> Result<Option<Vec<u8>>, anyhow::Error> {
319 match BASE64_ENGINE.decode(data.as_ref()) {
320 Ok(decoded) => Ok(Some(self.blocking_decrypt_raw(decoded)?)),
321 Err(_) => Ok(None),
322 }
323 }
324
325 #[inline]
326 pub async fn batch_action(
327 &self,
328 key: &'static str,
329 uuid: uuid::Uuid,
330 action: impl Future<Output = Result<(), anyhow::Error>> + Send + 'static,
331 ) {
332 let mut actions = self.batch_actions.lock().await;
333 actions.insert((key, uuid), Box::pin(action));
334 }
335}
336
337#[derive(Debug)]
338pub enum DatabaseError {
339 Sqlx(sqlx::Error),
340 Mongodb(mongodb::error::Error),
341 Serde(serde_json::Error),
342 Any(anyhow::Error),
343 Validation(garde::Report),
344 InvalidRelation(InvalidRelationError),
345}
346
347impl Display for DatabaseError {
348 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349 match self {
350 Self::Sqlx(sqlx_value) => sqlx_value.fmt(f),
351 Self::Mongodb(mongodb_value) => mongodb_value.fmt(f),
352 Self::Serde(serde_value) => serde_value.fmt(f),
353 Self::Any(any_value) => any_value.fmt(f),
354 Self::Validation(validation_value) => validation_value.fmt(f),
355 Self::InvalidRelation(relation_value) => relation_value.fmt(f),
356 }
357 }
358}
359
360impl From<wings_api::client::ApiHttpError> for DatabaseError {
361 #[inline]
362 fn from(value: wings_api::client::ApiHttpError) -> Self {
363 Self::Any(value.into())
364 }
365}
366
367impl From<anyhow::Error> for DatabaseError {
368 #[inline]
369 fn from(value: anyhow::Error) -> Self {
370 Self::Any(value)
371 }
372}
373
374impl From<serde_json::Error> for DatabaseError {
375 #[inline]
376 fn from(value: serde_json::Error) -> Self {
377 Self::Serde(value)
378 }
379}
380
381impl From<sqlx::Error> for DatabaseError {
382 #[inline]
383 fn from(value: sqlx::Error) -> Self {
384 Self::Sqlx(value)
385 }
386}
387
388impl From<mongodb::error::Error> for DatabaseError {
389 #[inline]
390 fn from(value: mongodb::error::Error) -> Self {
391 Self::Mongodb(value)
392 }
393}
394
395impl From<garde::Report> for DatabaseError {
396 #[inline]
397 fn from(value: garde::Report) -> Self {
398 Self::Validation(value)
399 }
400}
401
402impl From<InvalidRelationError> for DatabaseError {
403 fn from(value: InvalidRelationError) -> Self {
404 Self::InvalidRelation(value)
405 }
406}
407
408impl DatabaseError {
409 #[inline]
410 pub fn is_unique_constraint_violation(&self, constraint: &str) -> bool {
411 match self {
412 Self::Sqlx(sqlx_value) => sqlx_value
413 .as_database_error()
414 .is_some_and(|e| e.is_unique_violation() && e.constraint() == Some(constraint)),
415 _ => false,
416 }
417 }
418
419 #[inline]
420 pub fn is_unique_violation(&self) -> bool {
421 match self {
422 Self::Sqlx(sqlx_value) => sqlx_value
423 .as_database_error()
424 .is_some_and(|e| e.is_unique_violation()),
425 _ => false,
426 }
427 }
428
429 #[inline]
430 pub fn is_foreign_key_violation(&self) -> bool {
431 match self {
432 Self::Sqlx(sqlx_value) => sqlx_value
433 .as_database_error()
434 .is_some_and(|e| e.is_foreign_key_violation()),
435 _ => false,
436 }
437 }
438
439 #[inline]
440 pub fn is_check_violation(&self) -> bool {
441 match self {
442 Self::Sqlx(sqlx_value) => sqlx_value
443 .as_database_error()
444 .is_some_and(|e| e.is_check_violation()),
445 _ => false,
446 }
447 }
448
449 #[inline]
450 pub const fn is_validation_error(&self) -> bool {
451 matches!(self, Self::Validation(_))
452 }
453
454 #[inline]
455 pub const fn is_invalid_relation(&self) -> bool {
456 matches!(self, Self::InvalidRelation(_))
457 }
458}
459
460impl std::error::Error for DatabaseError {}
461
462#[derive(Debug)]
463pub struct InvalidRelationError(pub &'static str);
464
465impl Display for InvalidRelationError {
466 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
467 write!(f, "invalid relation `{}` provided", self.0)
468 }
469}