You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-11-20 12:02:22 +03:00
storage{,-pg}: better documentation of both crates
This commit is contained in:
@@ -18,6 +18,152 @@
|
||||
//! type-checked, using introspection data recorded in the `sqlx-data.json`
|
||||
//! file. This file is generated by the `sqlx` CLI tool, and should be updated
|
||||
//! whenever the database schema changes, or new queries are added.
|
||||
//!
|
||||
//! # Implementing a new repository
|
||||
//!
|
||||
//! When a new repository is defined in [`mas_storage`], it should be
|
||||
//! implemented here, with the PostgreSQL backend.
|
||||
//!
|
||||
//! A typical implementation will look like this:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use async_trait::async_trait;
|
||||
//! # use ulid::Ulid;
|
||||
//! # use rand::RngCore;
|
||||
//! # use mas_storage::Clock;
|
||||
//! # use mas_storage_pg::{DatabaseError, ExecuteExt, LookupResultExt};
|
||||
//! # use sqlx::PgConnection;
|
||||
//! # use uuid::Uuid;
|
||||
//! #
|
||||
//! # // A fake data structure, usually defined in mas-data-model
|
||||
//! # #[derive(sqlx::FromRow)]
|
||||
//! # struct FakeData {
|
||||
//! # id: Ulid,
|
||||
//! # }
|
||||
//! #
|
||||
//! # // A fake repository trait, usually defined in mas-storage
|
||||
//! # #[async_trait]
|
||||
//! # pub trait FakeDataRepository: Send + Sync {
|
||||
//! # type Error;
|
||||
//! # async fn lookup(&mut self, id: Ulid) -> Result<Option<FakeData>, Self::Error>;
|
||||
//! # async fn add(
|
||||
//! # &mut self,
|
||||
//! # rng: &mut (dyn RngCore + Send),
|
||||
//! # clock: &dyn Clock,
|
||||
//! # ) -> Result<FakeData, Self::Error>;
|
||||
//! # }
|
||||
//! #
|
||||
//! /// An implementation of [`FakeDataRepository`] for a PostgreSQL connection
|
||||
//! pub struct PgFakeDataRepository<'c> {
|
||||
//! conn: &'c mut PgConnection,
|
||||
//! }
|
||||
//!
|
||||
//! impl<'c> PgFakeDataRepository<'c> {
|
||||
//! /// Create a new [`FakeDataRepository`] from an active PostgreSQL connection
|
||||
//! pub fn new(conn: &'c mut PgConnection) -> Self {
|
||||
//! Self { conn }
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(sqlx::FromRow)]
|
||||
//! struct FakeDataLookup {
|
||||
//! fake_data_id: Uuid,
|
||||
//! }
|
||||
//!
|
||||
//! impl From<FakeDataLookup> for FakeData {
|
||||
//! fn from(value: FakeDataLookup) -> Self {
|
||||
//! Self {
|
||||
//! id: value.fake_data_id.into(),
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! #[async_trait]
|
||||
//! impl<'c> FakeDataRepository for PgFakeDataRepository<'c> {
|
||||
//! type Error = DatabaseError;
|
||||
//!
|
||||
//! #[tracing::instrument(
|
||||
//! name = "db.fake_data.lookup",
|
||||
//! skip_all,
|
||||
//! fields(
|
||||
//! db.statement,
|
||||
//! fake_data.id = %id,
|
||||
//! ),
|
||||
//! err,
|
||||
//! )]
|
||||
//! async fn lookup(&mut self, id: Ulid) -> Result<Option<FakeData>, Self::Error> {
|
||||
//! // Note: here we would use the macro version instead, but it's not possible here in
|
||||
//! // this documentation example
|
||||
//! let res: Option<FakeDataLookup> = sqlx::query_as(
|
||||
//! r#"
|
||||
//! SELECT fake_data_id
|
||||
//! FROM fake_data
|
||||
//! WHERE fake_data_id = $1
|
||||
//! "#,
|
||||
//! )
|
||||
//! .bind(Uuid::from(id))
|
||||
//! .traced()
|
||||
//! .fetch_one(&mut *self.conn)
|
||||
//! .await
|
||||
//! .to_option()?;
|
||||
//!
|
||||
//! let Some(res) = res else { return Ok(None) };
|
||||
//!
|
||||
//! Ok(Some(res.into()))
|
||||
//! }
|
||||
//!
|
||||
//! #[tracing::instrument(
|
||||
//! name = "db.fake_data.add",
|
||||
//! skip_all,
|
||||
//! fields(
|
||||
//! db.statement,
|
||||
//! fake_data.id,
|
||||
//! ),
|
||||
//! err,
|
||||
//! )]
|
||||
//! async fn add(
|
||||
//! &mut self,
|
||||
//! rng: &mut (dyn RngCore + Send),
|
||||
//! clock: &dyn Clock,
|
||||
//! ) -> Result<FakeData, Self::Error> {
|
||||
//! let created_at = clock.now();
|
||||
//! let id = Ulid::from_datetime_with_source(created_at.into(), rng);
|
||||
//! tracing::Span::current().record("fake_data.id", tracing::field::display(id));
|
||||
//!
|
||||
//! // Note: here we would use the macro version instead, but it's not possible here in
|
||||
//! // this documentation example
|
||||
//! sqlx::query(
|
||||
//! r#"
|
||||
//! INSERT INTO fake_data (id)
|
||||
//! VALUES ($1)
|
||||
//! "#,
|
||||
//! )
|
||||
//! .bind(Uuid::from(id))
|
||||
//! .traced()
|
||||
//! .execute(&mut *self.conn)
|
||||
//! .await?;
|
||||
//!
|
||||
//! Ok(FakeData {
|
||||
//! id,
|
||||
//! })
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! A few things to note with the implementation:
|
||||
//!
|
||||
//! - All methods are traced, with an explicit, somewhat consistent name.
|
||||
//! - The SQL statement is included as attribute, by declaring a `db.statement`
|
||||
//! attribute on the tracing span, and then calling [`ExecuteExt::traced`].
|
||||
//! - The IDs are all [`Ulid`], and generated from the clock and the random
|
||||
//! number generated passed as parameters. The generated IDs are recorded in
|
||||
//! the span.
|
||||
//! - The IDs are stored as [`Uuid`] in PostgreSQL, so conversions are required
|
||||
//! - "Not found" errors are handled by returning `Ok(None)` instead of an
|
||||
//! error. The [`LookupResultExt::to_option`] method helps to do that.
|
||||
//!
|
||||
//! [`Ulid`]: ulid::Ulid
|
||||
//! [`Uuid`]: uuid::Uuid
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(
|
||||
@@ -30,17 +176,23 @@
|
||||
#![warn(clippy::pedantic)]
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
|
||||
use sqlx::{migrate::Migrator, postgres::PgQueryResult};
|
||||
use thiserror::Error;
|
||||
use ulid::Ulid;
|
||||
use sqlx::migrate::Migrator;
|
||||
|
||||
/// An extension trait for [`Result`] which adds a [`to_option`] method, useful
|
||||
/// for handling "not found" errors from [`sqlx`]
|
||||
trait LookupResultExt {
|
||||
///
|
||||
/// [`to_option`]: LookupResultExt::to_option
|
||||
pub trait LookupResultExt {
|
||||
/// The output type
|
||||
type Output;
|
||||
|
||||
/// Transform a [`Result`] from a sqlx query to transform "not found" errors
|
||||
/// into [`None`]
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns the original error if the error was not a
|
||||
/// [`sqlx::Error::RowNotFound`] error
|
||||
fn to_option(self) -> Result<Option<Self::Output>, sqlx::Error>;
|
||||
}
|
||||
|
||||
@@ -56,143 +208,18 @@ impl<T> LookupResultExt for Result<T, sqlx::Error> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic error when interacting with the database
|
||||
#[derive(Debug, Error)]
|
||||
#[error(transparent)]
|
||||
pub enum DatabaseError {
|
||||
/// An error which came from the database itself
|
||||
Driver {
|
||||
/// The underlying error from the database driver
|
||||
#[from]
|
||||
source: sqlx::Error,
|
||||
},
|
||||
|
||||
/// An error which occured while converting the data from the database
|
||||
Inconsistency(#[from] DatabaseInconsistencyError),
|
||||
|
||||
/// An error which happened because the requested database operation is
|
||||
/// invalid
|
||||
#[error("Invalid database operation")]
|
||||
InvalidOperation {
|
||||
/// The source of the error, if any
|
||||
#[source]
|
||||
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
|
||||
},
|
||||
|
||||
/// An error which happens when an operation affects not enough or too many
|
||||
/// rows
|
||||
#[error("Expected {expected} rows to be affected, but {actual} rows were affected")]
|
||||
RowsAffected {
|
||||
/// How many rows were expected to be affected
|
||||
expected: u64,
|
||||
|
||||
/// How many rows were actually affected
|
||||
actual: u64,
|
||||
},
|
||||
}
|
||||
|
||||
impl DatabaseError {
|
||||
pub(crate) fn ensure_affected_rows(
|
||||
result: &PgQueryResult,
|
||||
expected: u64,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let actual = result.rows_affected();
|
||||
if actual == expected {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(DatabaseError::RowsAffected { expected, actual })
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_invalid_operation<E: std::error::Error + Send + Sync + 'static>(e: E) -> Self {
|
||||
Self::InvalidOperation {
|
||||
source: Some(Box::new(e)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn invalid_operation() -> Self {
|
||||
Self::InvalidOperation { source: None }
|
||||
}
|
||||
}
|
||||
|
||||
/// An error which occured while converting the data from the database
|
||||
#[derive(Debug, Error)]
|
||||
pub struct DatabaseInconsistencyError {
|
||||
/// The table which was being queried
|
||||
table: &'static str,
|
||||
|
||||
/// The column which was being queried
|
||||
column: Option<&'static str>,
|
||||
|
||||
/// The row which was being queried
|
||||
row: Option<Ulid>,
|
||||
|
||||
/// The source of the error
|
||||
#[source]
|
||||
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DatabaseInconsistencyError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Database inconsistency on table {}", self.table)?;
|
||||
if let Some(column) = self.column {
|
||||
write!(f, " column {column}")?;
|
||||
}
|
||||
if let Some(row) = self.row {
|
||||
write!(f, " row {row}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl DatabaseInconsistencyError {
|
||||
/// Create a new [`DatabaseInconsistencyError`] for the given table
|
||||
#[must_use]
|
||||
pub(crate) const fn on(table: &'static str) -> Self {
|
||||
Self {
|
||||
table,
|
||||
column: None,
|
||||
row: None,
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the column which was being queried
|
||||
#[must_use]
|
||||
pub(crate) const fn column(mut self, column: &'static str) -> Self {
|
||||
self.column = Some(column);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the row which was being queried
|
||||
#[must_use]
|
||||
pub(crate) const fn row(mut self, row: Ulid) -> Self {
|
||||
self.row = Some(row);
|
||||
self
|
||||
}
|
||||
|
||||
/// Give the source of the error
|
||||
#[must_use]
|
||||
pub(crate) fn source<E: std::error::Error + Send + Sync + 'static>(
|
||||
mut self,
|
||||
source: E,
|
||||
) -> Self {
|
||||
self.source = Some(Box::new(source));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub mod compat;
|
||||
pub mod oauth2;
|
||||
pub mod upstream_oauth2;
|
||||
pub mod user;
|
||||
|
||||
mod errors;
|
||||
pub(crate) mod pagination;
|
||||
pub(crate) mod repository;
|
||||
pub(crate) mod tracing;
|
||||
|
||||
pub use self::repository::PgRepository;
|
||||
pub(crate) use self::errors::DatabaseInconsistencyError;
|
||||
pub use self::{errors::DatabaseError, repository::PgRepository, tracing::ExecuteExt};
|
||||
|
||||
/// Embedded migrations, allowing them to run on startup
|
||||
pub static MIGRATOR: Migrator = sqlx::migrate!();
|
||||
|
||||
Reference in New Issue
Block a user