Skip to content

Commit

Permalink
feat: Add SeaORM migrations and utils to create user table (#284)
Browse files Browse the repository at this point in the history
It is pretty common that an application will need a `user` table in its
DB. This PR adds a sensible default schema via SeaORM migrations,
collected into a common `UserMigration` struct (that implements SeaORM's
`MigratorTrait`).

Also, add the migrations in the `full` example to demonstrate usage and
what the generated entity structs look like.
  • Loading branch information
spencewenski authored Jul 23, 2024
1 parent 741c0ef commit 50a17b2
Show file tree
Hide file tree
Showing 19 changed files with 450 additions and 1 deletion.
4 changes: 4 additions & 0 deletions examples/full/entity/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.7
pub mod prelude;

pub mod user;
3 changes: 3 additions & 0 deletions examples/full/entity/src/prelude.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.7
pub use super::user::Entity as User;
33 changes: 33 additions & 0 deletions examples/full/entity/src/user.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0-rc.7
use sea_orm::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "user")]
pub struct Model {
pub created_at: DateTimeWithTimeZone,
pub updated_at: DateTimeWithTimeZone,
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub name: String,
#[sea_orm(unique)]
pub username: String,
#[sea_orm(unique)]
pub email: String,
pub password: String,
pub email_confirmation_sent_at: Option<DateTimeWithTimeZone>,
pub email_confirmation_token: Option<String>,
pub email_confirmed_at: Option<DateTimeWithTimeZone>,
pub last_sign_in_at: Option<DateTimeWithTimeZone>,
pub recovery_sent_at: Option<DateTimeWithTimeZone>,
pub recovery_token: Option<String>,
pub email_change_sent_at: Option<DateTimeWithTimeZone>,
pub email_change_token_new: Option<String>,
pub email_change_token_current: Option<String>,
pub deleted_at: Option<DateTimeWithTimeZone>,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

impl ActiveModelBehavior for ActiveModel {}
1 change: 1 addition & 0 deletions examples/full/migration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ path = "src/lib.rs"

[dependencies]
tokio = { workspace = true }
roadster = { path = "../../..", default-features = false, features = ["db-sql"] }

[dependencies.sea-orm-migration]
version = "1.0.0-rc.5"
Expand Down
9 changes: 8 additions & 1 deletion examples/full/migration/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use roadster::migration::user::UserMigrator;
pub use sea_orm_migration::prelude::*;

mod m20220101_000001_create_table;
Expand All @@ -7,6 +8,12 @@ pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20220101_000001_create_table::Migration)]
let migrations: Vec<Box<dyn MigrationTrait>> =
vec![Box::new(m20220101_000001_create_table::Migration)];

migrations
.into_iter()
.chain(UserMigrator::migrations())
.collect()
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ pub mod config;
pub mod error;
pub mod health_check;
pub mod middleware;
#[cfg(feature = "db-sql")]
pub mod migration;
pub mod service;
#[cfg(any(test, feature = "testing"))]
pub mod testing;
Expand Down
28 changes: 28 additions & 0 deletions src/migration/check.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//! Utility methods to add check constraints to columns.
use sea_orm_migration::prelude::*;

/// Expression to check that a string column value is not empty.
pub fn str_not_empty<T>(name: T) -> SimpleExpr
where
T: IntoIden + 'static,
{
str_len_gt(name, 0)
}

/// Expression to check that a string column value's length is greater than the provided value.
pub fn str_len_gt<T>(name: T, len: u64) -> SimpleExpr
where
T: IntoIden + 'static,
{
Expr::expr(Func::char_length(Expr::col(name))).gt(len)
}

/// Expression to check that a string column value's length is greater than or equal to the
/// provided value.
pub fn str_len_gte<T>(name: T, len: u64) -> SimpleExpr
where
T: IntoIden + 'static,
{
Expr::expr(Func::char_length(Expr::col(name))).gte(len)
}
8 changes: 8 additions & 0 deletions src/migration/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//! This module provides pre-built SeaORM migrations for table schemas that are applicable
//! across many different applications and problem spaces.
//!
//! Additionally, some utilities are provided to create some common column types.
pub mod check;
pub mod schema;
pub mod user;
92 changes: 92 additions & 0 deletions src/migration/schema.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//! Utility methods to create common column types in table create/alter statements.
//!
//! These utilities are similar to the ones provided by [SeaORM][sea_orm_migration::schema] and
//! [Loco](https://github.com/loco-rs/loco/blob/be7ead6e2503731aea252ed8dc6542d74f2c2e4f/src/schema.rs),
//! but with some minor differences. For example, our updated/created at timestamps include the
//! timezone, while SeaORM/Loco do not.
use sea_orm_migration::{prelude::*, schema::*};

/// Timestamp related fields.
#[derive(DeriveIden)]
#[non_exhaustive]
pub enum Timetamps {
/// When the row was created. When used with the [timestamps] method, will default to
/// the current timestamp (with timezone).
CreatedAt,
/// When the row was updated. When used with the [timestamps] method, will be initially set to
/// the current timestamp (with timezone). Updates to the row will need to provide this field
/// as well in order for it to be updated.
// Todo: does seaorm automatically update this?
UpdatedAt,
}

/// Create a table if it does not exist yet and add some default columns
/// (e.g., create/update timestamps).
pub fn table<T: IntoIden + 'static>(name: T) -> TableCreateStatement {
timestamps(Table::create().table(name).if_not_exists().to_owned())
}

/// Add "timestamp with time zone" columns (`CreatedAt` and `UpdatedAt`) to a table.
/// The default for each column is the current timestamp.
pub fn timestamps(mut table: TableCreateStatement) -> TableCreateStatement {
table
.col(timestamp_with_time_zone(Timetamps::CreatedAt).default(Expr::current_timestamp()))
.col(timestamp_with_time_zone(Timetamps::UpdatedAt).default(Expr::current_timestamp()))
.to_owned()
}

/// Create an auto-incrementing primary key column using [BigInteger][sea_orm::sea_query::ColumnType::BigInteger]
/// as the column type.
pub fn pk_bigint_auto<T>(name: T) -> ColumnDef
where
T: IntoIden,
{
big_integer(name).primary_key().auto_increment().to_owned()
}

/// Create a primary key column using [Uuid][sea_orm::sea_query::ColumnType::Uuid] as the column
/// type. No default value is provided, so it needs to be generated/provided by the application.
pub fn pk_uuid<T>(name: T) -> ColumnDef
where
T: IntoIden,
{
ColumnDef::new(name).uuid().primary_key().to_owned()
}

/// Create a primary key column using [Uuid][sea_orm::sea_query::ColumnType::Uuid] as the column
/// type. A new v4 UUID will be generated as the default if no value is provided by the application.
///
/// Note: This requires that your database supports generating v4 UUIDs using a method named
/// `uuid_generate_v4()`.
pub fn pk_uuidv4<T>(name: T) -> ColumnDef
where
T: IntoIden,
{
pk_uuid_default(name, Expr::cust("uuid_generate_v4()"))
}

/// Create a primary key column using [Uuid][sea_orm::sea_query::ColumnType::Uuid] as the column
/// type. A new v7 UUID will be generated as the default if no value is provided by the application.
///
/// Note: This requires that your database supports generating v7 UUIDs using a method named
/// `uuid_generate_v7()`.
pub fn pk_uuidv7<T>(name: T) -> ColumnDef
where
T: IntoIden,
{
pk_uuid_default(name, Expr::cust("uuid_generate_v7()"))
}

/// Create a primary key column using [Uuid][sea_orm::sea_query::ColumnType::Uuid] as the column
/// type.
///
/// Provide a `default` expression in order to define how a default value is generated if no value
/// is not provided by the application.
pub fn pk_uuid_default<T, D>(name: T, default: D) -> ColumnDef
where
T: IntoIden,
D: Into<SimpleExpr>,
{
pk_uuid(name).default(default).to_owned()
}
56 changes: 56 additions & 0 deletions src/migration/user/create_table.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use crate::migration::check::str_not_empty;
use crate::migration::schema::{pk_bigint_auto, pk_uuid, table};
use crate::migration::user::User;
use sea_orm_migration::{prelude::*, schema::*};

pub(crate) fn create_table_uuid_pk() -> TableCreateStatement {
create_table(pk_uuid(User::Id))
}

pub(crate) fn create_table_int_pk() -> TableCreateStatement {
create_table(pk_bigint_auto(User::Id))
}

pub(crate) fn create_table(pk_col: ColumnDef) -> TableCreateStatement {
table(User::Table)
.col(pk_col)
.col(string(User::Name).check(str_not_empty(User::Name)))
.col(string_uniq(User::Username).check(str_not_empty(User::Username)))
.col(string_uniq(User::Email).check(str_not_empty(User::Email)))
.col(string(User::Password))
.to_owned()
}

pub(crate) fn drop_table() -> TableDropStatement {
Table::drop().table(User::Table).to_owned()
}

#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use sea_orm::sea_query::PostgresQueryBuilder;

#[test]
#[cfg_attr(coverage_nightly, coverage(off))]
fn create_table_uuid_pk() {
let query = super::create_table_uuid_pk();

assert_snapshot!(query.to_string(PostgresQueryBuilder));
}

#[test]
#[cfg_attr(coverage_nightly, coverage(off))]
fn create_table_int_pk() {
let query = super::create_table_int_pk();

assert_snapshot!(query.to_string(PostgresQueryBuilder));
}

#[test]
#[cfg_attr(coverage_nightly, coverage(off))]
fn drop_table() {
let query = super::drop_table();

assert_snapshot!(query.to_string(PostgresQueryBuilder));
}
}
27 changes: 27 additions & 0 deletions src/migration/user/m20240714_203550_create_user_table_int_pk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//! Migrations to create a basic `user` table that contains the following fields:
//!
//! - Id (BIGINT)
//! - Name
//! - Username
//! - Email
//! - Password
//!
//! To add more fields, use the other migrations in the `user` mod.
use crate::migration::user::create_table::{create_table_int_pk, drop_table};
use sea_orm_migration::prelude::*;

#[derive(Default, DeriveMigrationName)]
#[non_exhaustive]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.create_table(create_table_int_pk()).await
}

async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.drop_table(drop_table()).await
}
}
27 changes: 27 additions & 0 deletions src/migration/user/m20240714_203551_create_user_table_uuid_pk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//! Migrations to create a basic `user` table that contains the following fields:
//!
//! - Id (UUID)
//! - Name
//! - Username
//! - Email
//! - Password
//!
//! To add more fields, use the other migrations in the `user` mod.
use crate::migration::user::create_table::{create_table_uuid_pk, drop_table};
use sea_orm_migration::prelude::*;

#[derive(Default, DeriveMigrationName)]
#[non_exhaustive]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.create_table(create_table_uuid_pk()).await
}

async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.drop_table(drop_table()).await
}
}
Loading

0 comments on commit 50a17b2

Please sign in to comment.