Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add SeaORM migrations and utils to create user table #284

Merged
merged 1 commit into from
Jul 23, 2024
Merged
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
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
}
}
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