Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions blueprint/cli/blueprints/entity-test-helper/file.rs.liquid.liquid
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
//! This entity test helper for [`{{entity_struct_name}}`] allows making specific database access functions available
//! only for application tests but not for actual application code.

use crate::entities::{{entity_plural_name}}::{{entity_struct_name}};
use fake::{faker::name::en::*, Dummy};
use sqlx::postgres::PgPool;
use validator::Validate;

/// A changeset representing the data intended to be used to either create a new [`{{entity_struct_name}}`] or update an existing [`{{entity_struct_name}}`].
///
/// Changesets are validated in the [`create`] and [`update`] functions which return an [Result::Err] if validation fails.
///
/// Changesets can also be used to generate fake data for tests when the `test-helpers` feature is enabled:
///
/// ```
/// let task_changeset: TaskChangeset = Faker.fake();
/// ```
#[derive(Debug, Clone, Dummy, Validate)]
pub struct {{entity_struct_name}}Changeset {
// these are examples only
Expand All @@ -11,6 +23,11 @@ pub struct {{entity_struct_name}}Changeset {
pub name: String,
}

/// Create a [`{{entity_struct_name}}`] in the database with the data in the passed [`{{entity_struct_name}}Changeset`].
///
/// # Errors
/// - If there is a generic database error, a [`crate::Error::DbError`] will be returned.
/// - If the data in the changeset isn't valid, a [`crate::Error::ValidationError`] will be returned, otherwise the created [`{{entity_struct_name}}`] is returned.
pub async fn create({{entity_singular_name}}: {{entity_struct_name}}Changeset, db: &PgPool) -> Result<{{entity_struct_name}}, anyhow::Error> {
todo!("Adopt the SQL query as necessary!");
let record = sqlx::query!(
Expand Down
33 changes: 33 additions & 0 deletions blueprint/cli/blueprints/entity/file.rs.liquid.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ pub struct {{entity_struct_name}} {
{%- endfor %}
}

/// A changeset representing the data intended to be used to either create a new [`{{entity_struct_name}}`] or update an existing [`{{entity_struct_name}}`].
///
/// Changesets are validated in the [`create`] and [`update`] functions which return an [`Result::Err`] if validation fails.
///
/// Changesets can also be used to generate fake data for tests when the `test-helpers` feature is enabled:
///
/// ```
/// let task_changeset: TaskChangeset = Faker.fake();
/// ```
#[derive(Deserialize, Validate, Clone)]
#[cfg_attr(feature = "test-helpers", derive(Serialize, Dummy))]
pub struct {{entity_struct_name}}Changeset {
Expand All @@ -24,6 +33,10 @@ pub struct {{entity_struct_name}}Changeset {
{%- endfor %}
}

/// Loads all [`{{entity_struct_name}}`]s from the database.
///
/// # Errors
/// - If there is a generic database error, a [`crate::Error::DbError`] will be returned.
pub async fn load_all(
executor: impl sqlx::Executor<'_, Database = Postgres>,
) -> Result<Vec<{{entity_struct_name}}>, crate::Error> {
Expand All @@ -33,6 +46,11 @@ pub async fn load_all(
Ok({{entity_plural_name}})
}

/// Load one [`{{entity_struct_name}}`] from the database identified by its ID.
///
/// # Errors
/// - If there is a generic database error, a [`crate::Error::DbError`] will be returned.
/// - If no record can be found for the ID, a [`crate::Error::NoRecordFound`] will be returned.
pub async fn load(
id: Uuid,
executor: impl sqlx::Executor<'_, Database = Postgres>,
Expand All @@ -51,6 +69,11 @@ pub async fn load(
}
}

/// Create a [`{{entity_struct_name}}`] in the database with the data in the passed [`{{entity_struct_name}}Changeset`].
///
/// # Errors
/// - If there is a generic database error, a [`crate::Error::DbError`] will be returned.
/// - If the data in the changeset isn't valid, a [`crate::Error::ValidationError`] will be returned, otherwise the created [`{{entity_struct_name}}`] is returned.
pub async fn create(
{{entity_singular_name}}: {{entity_struct_name}}Changeset,
executor: impl sqlx::Executor<'_, Database = Postgres>,
Expand All @@ -75,6 +98,11 @@ pub async fn create(
})
}

/// Updates a [`{{entity_struct_name}}`] in the database with the data in the passed [`{{entity_struct_name}}Changeset`].
///
/// # Errors
/// - If there is a generic database error, a [`crate::Error::DbError`] will be returned.
/// - If the data in the changeset isn't valid, a [`crate::Error::ValidationError`] will be returned, otherwise the updated [`Task`] is returned. If no record can be found for the ID, a [`crate::Error::NoRecordFound`] will be returned.
pub async fn update(
id: Uuid,
{{entity_singular_name}}: {{entity_struct_name}}Changeset,
Expand Down Expand Up @@ -103,6 +131,11 @@ pub async fn update(
}
}

/// Delete a [`{{entity_struct_name}}`] from the database identified by its ID.
///
/// # Errors
/// - If there is a generic database error, a [`crate::Error::DbError`] will be returned.
/// - If no record can be found for the ID, a [`crate::Error::NoRecordFound`] will be returned.
pub async fn delete(
id: Uuid,
executor: impl sqlx::Executor<'_, Database = Postgres>,
Expand Down
11 changes: 10 additions & 1 deletion blueprint/config/src/lib.rs.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,13 @@ pub struct DatabaseConfig {
/// you can set that location using the `APP_DOTENV_CONFIG_DIR` environment variable.
/// This is useful when they are mounted at separate locations in a Docker container, for example.
///
/// Configuration settings are loaded from these sources (in that order so that latter soruces override former):
/// Configuration settings are loaded from these sources (in that order so that latter sources override the former):
/// * the `config/app.toml` file
/// * the `config/environments/<development|production|test>.toml` files depending on the environment
/// * environment variables
///
/// # Errors
/// Returns an error if any of the configuration sources could not be read.
pub fn load_config<'a, T>(env: &Environment) -> Result<T, anyhow::Error>
where
T: Deserialize<'a>,
Expand Down Expand Up @@ -186,6 +189,9 @@ impl Display for Environment {
/// Returns the currently active environment.
///
/// If the `APP_ENVIRONMENT` env var is set, the application environment is parsed from that (which might fail if an invalid environment is set). If the env var is not set, [`Environment::Development`] is returned.
///
/// # Errors
/// Returns an error if the `APP_ENVIRONMENT` env var is set to an invalid environment.
pub fn get_env() -> Result<Environment, anyhow::Error> {
if let Ok(val) = env::var("APP_ENVIRONMENT") {
info!(r#"Setting environment from APP_ENVIRONMENT: "{}""#, val);
Expand All @@ -199,6 +205,9 @@ pub fn get_env() -> Result<Environment, anyhow::Error> {
/// Parses an [`Environment`] from a string.
///
/// The environment can be passed in different forms, e.g. "dev", "development", "prod", etc. If an invalid environment is passed, an error is returned.
///
/// # Errors
/// Returns an error if the environment is not one of `dev`, `development`, `prod`, `production` or `test`.
pub fn parse_env(env: &str) -> Result<Environment, anyhow::Error> {
let env = &env.to_lowercase();
match env.as_str() {
Expand Down
22 changes: 16 additions & 6 deletions blueprint/db/src/entities/tasks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ pub struct Task {
pub description: String,
}

/// A changeset representing the data that is intended to be used to either create a new task or update an existing task.
/// A changeset representing the data intended to be used to either create a new [`Task`] or update an existing [`Task`].
///
/// Changesets are validatated in the [`create`] and [`update`] functions which return an [Result::Err] if validation fails.
/// Changesets are validated in the [`create`] and [`update`] functions which return an [`Result::Err`] if validation fails.
///
/// Changesets can also be used to generate fake data for tests when the `test-helpers` feature is enabled:
///
Expand All @@ -34,6 +34,8 @@ pub struct TaskChangeset {
}

/// Load all [`Task`]s from the database.
/// # Errors
/// - If there is a generic database error, a [`crate::Error::DbError`] will be returned.
pub async fn load_all(
executor: impl sqlx::Executor<'_, Database = Postgres>,
) -> Result<Vec<Task>, crate::Error> {
Expand All @@ -45,7 +47,9 @@ pub async fn load_all(

/// Load one [`Task`] from the database identified by its ID.
///
/// If no record can be found for the ID, a [`crate::Error::NoRecordFound`] will be returned.
/// # Errors
/// - If there is a generic database error, a [`crate::Error::DbError`] will be returned.
/// - If no record can be found for the ID, a [`crate::Error::NoRecordFound`] will be returned.
pub async fn load(
id: Uuid,
executor: impl sqlx::Executor<'_, Database = Postgres>,
Expand All @@ -59,7 +63,9 @@ pub async fn load(

/// Delete a [`Task`] from the database identified by its ID.
///
/// If no record can be found for the ID, a [`crate::Error::NoRecordFound`] will be returned.
/// # Errors
/// - If there is a generic database error, a [`crate::Error::DbError`] will be returned.
/// - If no record can be found for the ID, a [`crate::Error::NoRecordFound`] will be returned.
pub async fn delete(
id: Uuid,
executor: impl sqlx::Executor<'_, Database = Postgres>,
Expand All @@ -75,7 +81,9 @@ pub async fn delete(

/// Create a task in the database with the data in the passed [`TaskChangeset`].
///
/// If the data in the changeset isn't valid, a [`crate::Error::ValidationError`] will be returned, otherwise the created task is returned.
/// # Errors
/// - If there is a generic database error, a [`crate::Error::DbError`] will be returned.
/// - If the data in the changeset isn't valid, a [`crate::Error::ValidationError`] will be returned, otherwise the created task is returned.
pub async fn create(
task: TaskChangeset,
executor: impl sqlx::Executor<'_, Database = Postgres>,
Expand All @@ -98,7 +106,9 @@ pub async fn create(

/// Updates a task in the database with the data in the passed [`TaskChangeset`].
///
/// If the data in the changeset isn't valid, a [`crate::Error::ValidationError`] will be returned, otherwise the updated [`Task`] is returned. If no record can be found for the ID, a [`crate::Error::NoRecordFound`] will be returned.
/// # Errors
/// - If there is a generic database error, a [`crate::Error::DbError`] will be returned.
/// - If the data in the changeset isn't valid, a [`crate::Error::ValidationError`] will be returned, otherwise the updated [`Task`] is returned. If no record can be found for the ID, a [`crate::Error::NoRecordFound`] will be returned.
pub async fn update(
id: Uuid,
task: TaskChangeset,
Expand Down
5 changes: 4 additions & 1 deletion blueprint/db/src/entities/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ pub struct User {
/// Loads a user based on the passed token.
///
/// If no user exists for the token, [`Option::None`] is returned, otherwise `Option::Some(User)` is returned.
///
/// # Errors
/// Returns [`crate::Error::DbError`] if a general database error occurs.
pub async fn load_with_token(
token: &str,
executor: impl sqlx::Executor<'_, Database = Postgres>,
) -> Result<Option<User>, anyhow::Error> {
) -> Result<Option<User>, crate::Error> {
Ok(
sqlx::query_as!(User, "SELECT id, name FROM users WHERE token = $1", token)
.fetch_optional(executor)
Expand Down
6 changes: 6 additions & 0 deletions blueprint/db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ pub mod entities;
/// ```
///
/// Transactions are rolled back automatically when they are dropped without having been committed.
///
/// # Errors
/// Returns a [`sqlx_core::error`] when the transaction is unsuccessful.
pub async fn transaction(
db_pool: &DbPool,
) -> Result<Transaction<'static, Postgres>, anyhow::Error> {
Expand Down Expand Up @@ -54,6 +57,9 @@ pub enum Error {
}

/// Creates a connection pool to the database specified in the passed [`{{project-name}}-config::DatabaseConfig`]
///
/// # Errors
/// Returns a [`sqlx_core::error`] when the transaction is unsuccessful.
pub async fn connect_pool(config: DatabaseConfig) -> Result<DbPool, anyhow::Error> {
let pool = PgPoolOptions::new()
.connect(config.url.as_str())
Expand Down
6 changes: 6 additions & 0 deletions blueprint/db/src/test_helpers/mod.rs.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ pub mod users;
///
/// This sets up a dedicated database as a fork of the main test database as configured in `.env.test`. The database can be used in a test case to ensure the test case is isolated from other test cases. The function returns a connection pool connected to the created database.
/// This function is automatically called by the [`{{project-name}}-macros::db_test`] macro. The return connection pool is passed to the test case via the [`{{project-name}}-macros::DbTestContext`].
///
/// # Panics
/// This function panics if it cannot connect to the database.
#[allow(unused)]
pub async fn setup_db(config: &DatabaseConfig) -> DbPool {
let test_db_config = prepare_db(config).await;
Expand All @@ -28,6 +31,9 @@ pub async fn setup_db(config: &DatabaseConfig) -> DbPool {
/// Drops a dedicated database for a test case.
///
/// This function is automatically called by the [`{{project-name}}-macros::db_test`] macro. It ensures test-specific database are cleaned up after each test run so we don't end up with large numbers of unused databases.
///
/// # Panics
/// This function panics if connection to the database fails, the database name cannot be retrieved, or the database cannot be dropped.
pub async fn teardown_db(db_pool: DbPool) {
let mut connect_options = db_pool.connect_options();
let db_config = Arc::make_mut(&mut connect_options);
Expand Down
3 changes: 2 additions & 1 deletion blueprint/db/src/test_helpers/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use sqlx::postgres::PgPool;

/// A changeset representing the data that is intended to be used to either create a new user or update an existing user.
///
/// Changesets are validated in the [`create`] function which return an [Result::Err] if validation fails.
/// Changesets are validated in the [`create`] function that returns an [`Result::Err`] if validation fails.
///
/// Changesets can also be used to generate fake data for tests when the `test-helpers` feature is enabled:
///
Expand All @@ -23,6 +23,7 @@ pub struct UserChangeset {

/// Creates a user in the database with the data in the passed [`UserChangeset`].
///
/// # Errors
/// If the data in the changeset isn't valid, a [`crate::Error::ValidationError`] will be returned, otherwise the created user is returned.
pub async fn create(user: UserChangeset, db: &PgPool) -> Result<User, anyhow::Error> {
let record = sqlx::query!(
Expand Down
31 changes: 26 additions & 5 deletions blueprint/web/src/controllers/tasks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ use uuid::Uuid;

/// Creates a task in the database.
///
/// This function creates a task in the database (see [`{{crate_name}}_db::entities::tasks::create`]) based on a [`{{crate_name}}_db::entities::tasks::TaskChangeset`] (sent as JSON). If the task is created successfully, a 201 response is returned with the created [`{{crate_name}}_db::entities::tasks::Task`]'s JSON representation in the response body. If the changeset is invalid, a 422 response is returned.
/// This function creates a task in the database (see [`{{crate_name}}_db::entities::tasks::create`]) based on a [`{{crate_name}}_db::entities::tasks::TaskChangeset`] (sent as JSON). If the task is created successfully, a 201 response is returned with the created [`{{crate_name}}_db::entities::tasks::Task`]'s JSON representation in the response body.
///
/// # Errors
/// - 422 Unprocessable Entity: Invalid changeset
#[axum::debug_handler]
pub async fn create(
State(app_state): State<SharedAppState>,
Expand All @@ -19,9 +22,12 @@ pub async fn create(

/// Creates multiple tasks in the database.
///
/// This function creates multiple tasks in the database (see [`{{crate_name}}_db::entities::tasks::create`]) based on [`{{crate_name}}_db::entities::tasks::TaskChangeset`]s (sent as JSON). If all tasks are created successfully, a 201 response is returned with the created [`{{crate_name}}_db::entities::tasks::Task`]s' JSON representation in the response body. If any of the passed changesets is invalid, a 422 response is returned.
/// This function creates multiple tasks in the database (see [`{{crate_name}}_db::entities::tasks::create`]) based on [`{{crate_name}}_db::entities::tasks::TaskChangeset`]s (sent as JSON). If all tasks are created successfully, a 201 response is returned with the created [`{{crate_name}}_db::entities::tasks::Task`]s' JSON representation in the response body.
///
/// This function creates all tasks in a transaction so that either all are created successfully or none is.
///
/// # Errors
/// - 422 Unprocessable Entity: Any changeset is invalid
#[axum::debug_handler]
pub async fn create_batch(
State(app_state): State<SharedAppState>,
Expand All @@ -43,6 +49,9 @@ pub async fn create_batch(
/// Reads and responds with all the tasks currently present in the database.
///
/// This function reads all [`{{crate_name}}_db::entities::tasks::Task`]s from the database (see [`{{crate_name}}_db::entities::tasks::load_all`]) and responds with their JSON representations.
///
/// # Errors
/// - 500 Internal Server Error: Database connection failed or other generic database error
#[axum::debug_handler]
pub async fn read_all(State(app_state): State<SharedAppState>) -> Result<Json<Vec<tasks::Task>>, Error> {
let tasks = tasks::load_all(&app_state.db_pool).await?;
Expand All @@ -54,7 +63,11 @@ pub async fn read_all(State(app_state): State<SharedAppState>) -> Result<Json<Ve

/// Reads and responds with a task identified by its ID.
///
/// This function reads one [`{{crate_name}}_db::entities::tasks::Task`] identified by its ID from the database (see [`{{crate_name}}_db::entities::tasks::load`]) and responds with its JSON representations. If no task is found for the ID, a 404 response is returned.
/// This function reads one [`{{crate_name}}_db::entities::tasks::Task`] identified by its ID from the database (see [`{{crate_name}}_db::entities::tasks::load`]) and responds with its JSON representations.
///
/// # Errors
/// - 404 Not found: No task with the given ID was found.
/// - 500 Internal Server Error: Database connection failed or other generic database error
#[axum::debug_handler]
pub async fn read_one(
State(app_state): State<SharedAppState>,
Expand All @@ -66,7 +79,11 @@ pub async fn read_one(

/// Updates a task in the database.
///
/// This function updates a task identified by its ID in the database (see [`{{crate_name}}_db::entities::tasks::update`]) with the data from the passed [`{{crate_name}}_db::entities::tasks::TaskChangeset`] (sent as JSON). If the task is updated successfully, a 200 response is returned with the created [`{{crate_name}}_db::entities::tasks::Task`]'s JSON representation in the response body. If the changeset is invalid, a 422 response is returned.
/// This function updates a task identified by its ID in the database (see [`{{crate_name}}_db::entities::tasks::update`]) with the data from the passed [`{{crate_name}}_db::entities::tasks::TaskChangeset`] (sent as JSON). If the task is updated successfully, a 200 response is returned with the created [`{{crate_name}}_db::entities::tasks::Task`]'s JSON representation in the response body.
///
/// # Errors
/// - 422 Unprocessable Entity: Invalid changeset
/// - 500 Internal Server Error: Database connection failed or other generic database error
#[axum::debug_handler]
pub async fn update(
State(app_state): State<SharedAppState>,
Expand All @@ -79,7 +96,11 @@ pub async fn update(

/// Deletes a task identified by its ID from the database.
///
/// This function deletes one [`{{crate_name}}_db::entities::tasks::Task`] identified by the entity's id from the database (see [`{{crate_name}}_db::entities::tasks::delete`]) and responds with a 204 status code and empty response body. If no task is found for the ID, a 404 response is returned.
/// This function deletes one [`{{crate_name}}_db::entities::tasks::Task`] identified by the entity's id from the database (see [`{{crate_name}}_db::entities::tasks::delete`]) and responds with a 204 status code and empty response body.
///
/// # Errors
/// - 404 Not found: No task with the given ID was found.
/// - 500 Internal Server Error: Database connection failed or other generic database error
#[axum::debug_handler]
pub async fn delete(
State(app_state): State<SharedAppState>,
Expand Down
Loading