diff --git a/drust/src/main.rs b/drust/src/main.rs index 576d4bdb..5a1dd565 100644 --- a/drust/src/main.rs +++ b/drust/src/main.rs @@ -3,13 +3,19 @@ use pagetop::prelude::*; struct Drust; impl AppTrait for Drust { - fn enable_modules(&self) -> Vec<&'static dyn ModuleTrait> { + fn enable_modules(&self) -> Vec { vec![ &pagetop_admin::Admin, &pagetop_user::User, &pagetop_node::Node, ] } + + fn disable_modules(&self) -> Vec { + vec![ + // &pagetop_node::Node, + ] + } } #[actix_web::main] diff --git a/pagetop/Cargo.toml b/pagetop/Cargo.toml index 3d07aed1..33b45256 100644 --- a/pagetop/Cargo.toml +++ b/pagetop/Cargo.toml @@ -22,6 +22,7 @@ categories = [ ] [dependencies] +async-trait = "0.1.56" concat-string = "1.0.1" doc-comment = "0.3.3" figlet-rs = "0.1.3" @@ -57,14 +58,13 @@ version = "0.9.1" features = ["debug-print", "macros", "runtime-async-std-native-tls"] default-features = false optional = true - -[dependencies.sea-orm-migration] -version = "0.9.1" +[dependencies.sea-schema] +version = "0.9.3" optional = true [features] default = [] -database = ["sea-orm", "sea-orm-migration"] +database = ["sea-orm", "sea-schema"] mysql = ["database", "sea-orm/sqlx-mysql"] postgres = ["database", "sea-orm/sqlx-postgres"] sqlite = ["database", "sea-orm/sqlx-sqlite"] diff --git a/pagetop/src/app/application.rs b/pagetop/src/app/application.rs index e68a0b95..8ecb2a84 100644 --- a/pagetop/src/app/application.rs +++ b/pagetop/src/app/application.rs @@ -27,6 +27,8 @@ impl Application { #[cfg(feature = "database")] LazyStatic::force(&super::db::DBCONN); + // Deshabilita los módulos indicados por la aplicación. + module::all::disable_modules(app.disable_modules()); // Habilita los módulos predeterminados. module::all::enable_modules(vec![&base::module::homepage::DefaultHomePage]); // Habilita los módulos de la aplicación. diff --git a/pagetop/src/app/definition.rs b/pagetop/src/app/definition.rs index 1d470a59..11eb50e6 100644 --- a/pagetop/src/app/definition.rs +++ b/pagetop/src/app/definition.rs @@ -1,18 +1,18 @@ -use crate::core::module::ModuleTrait; -use crate::core::theme::ThemeTrait; +use crate::core::module::ModuleStaticRef; +use crate::core::theme::ThemeStaticRef; pub trait AppTrait: Send + Sync { fn bootstrap(&self) {} - fn enable_modules(&self) -> Vec<&'static dyn ModuleTrait> { + fn enable_modules(&self) -> Vec { vec![] } - fn disable_modules(&self) -> Vec<&'static dyn ModuleTrait> { + fn disable_modules(&self) -> Vec { vec![] } - fn themes(&self) -> Vec<&'static dyn ThemeTrait> { + fn themes(&self) -> Vec { vec![] } } diff --git a/pagetop/src/base/component/form_element/date.rs b/pagetop/src/base/component/form_element/date.rs index 6945f9e6..35b8f5d8 100644 --- a/pagetop/src/base/component/form_element/date.rs +++ b/pagetop/src/base/component/form_element/date.rs @@ -24,7 +24,7 @@ pub struct Date { impl ComponentTrait for Date { fn new() -> Self { Date::default() - .with_classes(ClassesOp::SetDefault,"form-item") + .with_classes(ClassesOp::SetDefault, "form-item") .with_classes(ClassesOp::AddFirst, "form-type-date") } diff --git a/pagetop/src/base/component/menu.rs b/pagetop/src/base/component/menu.rs index 89da9f7b..2076d3fc 100644 --- a/pagetop/src/base/component/menu.rs +++ b/pagetop/src/base/component/menu.rs @@ -43,8 +43,8 @@ impl ComponentTrait for MenuItem { fn default_render(&self, context: &mut PageContext) -> Markup { match self.item_type() { - MenuItemType::Void => html! { - }, + MenuItemType::Void => html! {}, + MenuItemType::Label(label) => html! { li class="label" { a href="#" { (label) } } }, diff --git a/pagetop/src/core/component/html_markup.rs b/pagetop/src/core/component/html_markup.rs index c298c1cc..20eacf96 100644 --- a/pagetop/src/core/component/html_markup.rs +++ b/pagetop/src/core/component/html_markup.rs @@ -6,8 +6,6 @@ pub struct HtmlMarkup { impl Default for HtmlMarkup { fn default() -> Self { - HtmlMarkup { - markup: html! {}, - } + HtmlMarkup { markup: html! {} } } } diff --git a/pagetop/src/core/module.rs b/pagetop/src/core/module.rs index a47fe3c4..0ecfbc65 100644 --- a/pagetop/src/core/module.rs +++ b/pagetop/src/core/module.rs @@ -1,4 +1,4 @@ mod definition; -pub use definition::{BaseModule, ModuleTrait}; +pub use definition::{BaseModule, ModuleStaticRef, ModuleTrait}; pub(crate) mod all; diff --git a/pagetop/src/core/module/all.rs b/pagetop/src/core/module/all.rs index 803d1ac2..91220b52 100644 --- a/pagetop/src/core/module/all.rs +++ b/pagetop/src/core/module/all.rs @@ -1,4 +1,4 @@ -use super::ModuleTrait; +use super::ModuleStaticRef; use crate::core::hook::add_action; use crate::{app, trace, LazyStatic}; @@ -7,29 +7,39 @@ use crate::{db::*, run_now}; use std::sync::RwLock; -// Enabled modules. -static ENABLED_MODULES: LazyStatic>> = +// DISABLED MODULES ******************************************************************************** + +static DISABLED_MODULES: LazyStatic>> = LazyStatic::new(|| RwLock::new(Vec::new())); -/* Disabled modules. -static DISABLED_MODULES: Lazy>> = Lazy::new(|| { - RwLock::new(Vec::new()) -}); */ - -pub fn enable_modules(modules: Vec<&'static dyn ModuleTrait>) { - for m in modules { - enable(m) +pub fn disable_modules(modules: Vec) { + let mut disabled_modules = DISABLED_MODULES.write().unwrap(); + for module in modules { + if !disabled_modules + .iter() + .any(|m| m.handler() == module.handler()) + { + trace::debug!("Disabling the \"{}\" module", module.single_name()); + disabled_modules.push(module); + } } } -fn enable(module: &'static dyn ModuleTrait) { - let mut list: Vec<&dyn ModuleTrait> = Vec::new(); - add_to(&mut list, module); - list.reverse(); - ENABLED_MODULES.write().unwrap().append(&mut list); +// ENABLED MODULES ********************************************************************************* + +static ENABLED_MODULES: LazyStatic>> = + LazyStatic::new(|| RwLock::new(Vec::new())); + +pub fn enable_modules(modules: Vec) { + for module in modules { + let mut list: Vec = Vec::new(); + add_to_enabled(&mut list, module); + list.reverse(); + ENABLED_MODULES.write().unwrap().append(&mut list); + } } -fn add_to(list: &mut Vec<&dyn ModuleTrait>, module: &'static dyn ModuleTrait) { +fn add_to_enabled(list: &mut Vec, module: ModuleStaticRef) { if !ENABLED_MODULES .read() .unwrap() @@ -37,21 +47,31 @@ fn add_to(list: &mut Vec<&dyn ModuleTrait>, module: &'static dyn ModuleTrait) { .any(|m| m.handler() == module.handler()) && !list.iter().any(|m| m.handler() == module.handler()) { - trace::debug!("Enabling module \"{}\"", module.single_name()); - list.push(module); + if DISABLED_MODULES + .read() + .unwrap() + .iter() + .any(|m| m.handler() == module.handler()) + { + panic!( + "Trying to enable \"{}\" module which is disabled", + module.single_name() + ); + } else { + trace::debug!("Enabling the \"{}\" module", module.single_name()); + list.push(module); - let mut dependencies = module.dependencies(); - dependencies.reverse(); - for d in dependencies.iter() { - add_to(list, *d); + let mut dependencies = module.dependencies(); + dependencies.reverse(); + for d in dependencies.iter() { + add_to_enabled(list, *d); + } } } } -/* -#[allow(unused_variables)] -pub fn disable_module(module: &'static dyn ModuleTrait) { -} -*/ + +// CONFIGURE MODULES ******************************************************************************* + pub fn modules(cfg: &mut app::web::ServiceConfig) { for m in ENABLED_MODULES.read().unwrap().iter() { m.configure_service(cfg); @@ -68,6 +88,21 @@ pub fn register_actions() { #[cfg(feature = "database")] pub fn run_migrations() { + run_now({ + struct Migrator; + impl MigratorTrait for Migrator { + fn migrations() -> Vec { + let mut migrations = vec![]; + for m in DISABLED_MODULES.read().unwrap().iter() { + migrations.append(&mut m.migrations()); + } + migrations + } + } + Migrator::down(&app::db::DBCONN, None) + }) + .unwrap(); + run_now({ struct Migrator; impl MigratorTrait for Migrator { diff --git a/pagetop/src/core/module/definition.rs b/pagetop/src/core/module/definition.rs index e4492ab7..17105ba0 100644 --- a/pagetop/src/core/module/definition.rs +++ b/pagetop/src/core/module/definition.rs @@ -5,6 +5,8 @@ use crate::util::{single_type_name, Handler}; #[cfg(feature = "database")] use crate::db::MigrationItem; +pub type ModuleStaticRef = &'static dyn ModuleTrait; + pub trait BaseModule { fn single_name(&self) -> &'static str; } @@ -21,7 +23,7 @@ pub trait ModuleTrait: BaseModule + Send + Sync { None } - fn dependencies(&self) -> Vec<&'static dyn ModuleTrait> { + fn dependencies(&self) -> Vec { vec![] } diff --git a/pagetop/src/core/theme.rs b/pagetop/src/core/theme.rs index b481f69b..6e008d80 100644 --- a/pagetop/src/core/theme.rs +++ b/pagetop/src/core/theme.rs @@ -1,4 +1,4 @@ mod definition; -pub use definition::{BaseTheme, ThemeTrait}; +pub use definition::{BaseTheme, ThemeStaticRef, ThemeTrait}; pub(crate) mod all; diff --git a/pagetop/src/core/theme/all.rs b/pagetop/src/core/theme/all.rs index 256be72a..1f728ca8 100644 --- a/pagetop/src/core/theme/all.rs +++ b/pagetop/src/core/theme/all.rs @@ -1,4 +1,4 @@ -use super::ThemeTrait; +use super::ThemeStaticRef; use crate::{app, theme_static_files, trace, LazyStatic}; use std::sync::RwLock; @@ -6,24 +6,23 @@ use std::sync::RwLock; include!(concat!(env!("OUT_DIR"), "/theme.rs")); // Temas registrados. -static THEMES: LazyStatic>> = +static THEMES: LazyStatic>> = LazyStatic::new(|| RwLock::new(Vec::new())); -pub fn register_themes(themes: Vec<&'static dyn ThemeTrait>) { - for t in themes { - register(t) +pub fn register_themes(themes: Vec) { + let mut registered_themes = THEMES.write().unwrap(); + for theme in themes { + if !registered_themes + .iter() + .any(|t| t.handler() == theme.handler()) + { + trace::debug!("Registering theme \"{}\"", theme.single_name()); + registered_themes.push(theme); + } } } -fn register(theme: &'static dyn ThemeTrait) { - let mut themes = THEMES.write().unwrap(); - if !themes.iter().any(|t| t.handler() == theme.handler()) { - trace::debug!("Registering theme \"{}\"", theme.single_name()); - themes.push(theme); - } -} - -pub fn theme_by_single_name(single_name: &str) -> Option<&'static dyn ThemeTrait> { +pub fn theme_by_single_name(single_name: &str) -> Option { match THEMES .write() .unwrap() diff --git a/pagetop/src/core/theme/definition.rs b/pagetop/src/core/theme/definition.rs index ab996e6c..ee3abcbf 100644 --- a/pagetop/src/core/theme/definition.rs +++ b/pagetop/src/core/theme/definition.rs @@ -7,6 +7,8 @@ use crate::html::{html, Favicon, Markup}; use crate::response::page::{Page, PageContext, PageOp}; use crate::util::{single_type_name, Handler}; +pub type ThemeStaticRef = &'static dyn ThemeTrait; + pub trait BaseTheme { fn single_name(&self) -> &'static str; } diff --git a/pagetop/src/db.rs b/pagetop/src/db.rs index de2556ad..a6639943 100644 --- a/pagetop/src/db.rs +++ b/pagetop/src/db.rs @@ -2,7 +2,18 @@ pub use url::Url as DbUri; pub use sea_orm::{DatabaseConnection as DbConn, ExecResult, QueryResult}; -pub use sea_orm_migration::prelude::*; +// El siguiente módulo migration es una versión simplificada del módulo sea_orm_migration (v0.9.1) +// https://github.com/SeaQL/sea-orm/tree/0.9.1/sea-orm-migration para evitar los errores generados +// por el paradigma modular de PageTop. Se copian los siguientes archivos del original: +// +// lib.rs => db/migration.rs (descartando el uso de algunos módulos y exportaciones) +// manager.rs => db/migration/manager.rs +// migrator.rs => db/migration/migrator.rs (suprimiendo la gestión de los errores) +// prelude.rs => db/migration/prelude.rs (evitando cli) +// seaql_migrations.rs => db/migration/seaql_migrations.rs +// +mod migration; +pub use migration::prelude::*; pub type MigrationItem = Box; diff --git a/pagetop/src/db/migration.rs b/pagetop/src/db/migration.rs new file mode 100644 index 00000000..eb9ee6dc --- /dev/null +++ b/pagetop/src/db/migration.rs @@ -0,0 +1,30 @@ +//pub mod cli; +pub mod manager; +pub mod migrator; +pub mod prelude; +pub mod seaql_migrations; +//pub mod util; + +pub use manager::*; +//pub use migrator::*; + +//pub use async_trait; +//pub use sea_orm; +//pub use sea_orm::sea_query; +use sea_orm::DbErr; + +pub trait MigrationName { + fn name(&self) -> &str; +} + +/// The migration definition +#[async_trait::async_trait] +pub trait MigrationTrait: MigrationName + Send + Sync { + /// Define actions to perform when applying the migration + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr>; + + /// Define actions to perform when rolling back the migration + async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> { + Err(DbErr::Migration("We Don't Do That Here".to_owned())) + } +} diff --git a/pagetop/src/db/migration/manager.rs b/pagetop/src/db/migration/manager.rs new file mode 100644 index 00000000..71e91b37 --- /dev/null +++ b/pagetop/src/db/migration/manager.rs @@ -0,0 +1,133 @@ +use sea_orm::sea_query::{ + extension::postgres::{TypeAlterStatement, TypeCreateStatement, TypeDropStatement}, + ForeignKeyCreateStatement, ForeignKeyDropStatement, IndexCreateStatement, IndexDropStatement, + TableAlterStatement, TableCreateStatement, TableDropStatement, TableRenameStatement, + TableTruncateStatement, +}; +use sea_orm::{ConnectionTrait, DbBackend, DbConn, DbErr, StatementBuilder}; +use sea_schema::{mysql::MySql, postgres::Postgres, probe::SchemaProbe, sqlite::Sqlite}; + +/// Helper struct for writing migration scripts in migration file +pub struct SchemaManager<'c> { + conn: &'c DbConn, +} + +impl<'c> SchemaManager<'c> { + pub fn new(conn: &'c DbConn) -> Self { + Self { conn } + } + + pub async fn exec_stmt(&self, stmt: S) -> Result<(), DbErr> + where + S: StatementBuilder, + { + let builder = self.conn.get_database_backend(); + self.conn.execute(builder.build(&stmt)).await.map(|_| ()) + } + + pub fn get_database_backend(&self) -> DbBackend { + self.conn.get_database_backend() + } + + pub fn get_connection(&self) -> &'c DbConn { + self.conn + } +} + +/// Schema Creation +impl<'c> SchemaManager<'c> { + pub async fn create_table(&self, stmt: TableCreateStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn create_index(&self, stmt: IndexCreateStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn create_foreign_key(&self, stmt: ForeignKeyCreateStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn create_type(&self, stmt: TypeCreateStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } +} + +/// Schema Mutation +impl<'c> SchemaManager<'c> { + pub async fn alter_table(&self, stmt: TableAlterStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn drop_table(&self, stmt: TableDropStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn rename_table(&self, stmt: TableRenameStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn truncate_table(&self, stmt: TableTruncateStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn drop_index(&self, stmt: IndexDropStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn drop_foreign_key(&self, stmt: ForeignKeyDropStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn alter_type(&self, stmt: TypeAlterStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } + + pub async fn drop_type(&self, stmt: TypeDropStatement) -> Result<(), DbErr> { + self.exec_stmt(stmt).await + } +} + +/// Schema Inspection +impl<'c> SchemaManager<'c> { + pub async fn has_table(&self, table: T) -> Result + where + T: AsRef, + { + let stmt = match self.conn.get_database_backend() { + DbBackend::MySql => MySql::has_table(table), + DbBackend::Postgres => Postgres::has_table(table), + DbBackend::Sqlite => Sqlite::has_table(table), + }; + + let builder = self.conn.get_database_backend(); + let res = self + .conn + .query_one(builder.build(&stmt)) + .await? + .ok_or_else(|| DbErr::Custom("Failed to check table exists".to_owned()))?; + + res.try_get("", "has_table") + } + + pub async fn has_column(&self, table: T, column: C) -> Result + where + T: AsRef, + C: AsRef, + { + let stmt = match self.conn.get_database_backend() { + DbBackend::MySql => MySql::has_column(table, column), + DbBackend::Postgres => Postgres::has_column(table, column), + DbBackend::Sqlite => Sqlite::has_column(table, column), + }; + + let builder = self.conn.get_database_backend(); + let res = self + .conn + .query_one(builder.build(&stmt)) + .await? + .ok_or_else(|| DbErr::Custom("Failed to check column exists".to_owned()))?; + + res.try_get("", "has_column") + } +} diff --git a/pagetop/src/db/migration/migrator.rs b/pagetop/src/db/migration/migrator.rs new file mode 100644 index 00000000..ef881cae --- /dev/null +++ b/pagetop/src/db/migration/migrator.rs @@ -0,0 +1,326 @@ +use std::collections::HashSet; +use std::fmt::Display; +use std::time::SystemTime; +use tracing::info; + +use sea_orm::sea_query::{Alias, Expr, ForeignKey, Query, SelectStatement, SimpleExpr, Table}; +use sea_orm::{ + ActiveModelTrait, ActiveValue, ColumnTrait, Condition, ConnectionTrait, DbBackend, DbConn, + DbErr, EntityTrait, QueryFilter, QueryOrder, Schema, Statement, +}; +use sea_schema::{mysql::MySql, postgres::Postgres, probe::SchemaProbe, sqlite::Sqlite}; + +use super::{seaql_migrations, MigrationTrait, SchemaManager}; + +#[derive(Debug, PartialEq)] +/// Status of migration +pub enum MigrationStatus { + /// Not yet applied + Pending, + /// Applied + Applied, +} + +pub struct Migration { + migration: Box, + status: MigrationStatus, +} + +impl Display for MigrationStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let status = match self { + MigrationStatus::Pending => "Pending", + MigrationStatus::Applied => "Applied", + }; + write!(f, "{}", status) + } +} + +/// Performing migrations on a database +#[async_trait::async_trait] +pub trait MigratorTrait: Send { + /// Vector of migrations in time sequence + fn migrations() -> Vec>; + + /// Get list of migrations wrapped in `Migration` struct + fn get_migration_files() -> Vec { + Self::migrations() + .into_iter() + .map(|migration| Migration { + migration, + status: MigrationStatus::Pending, + }) + .collect() + } + + /// Get list of applied migrations from database + async fn get_migration_models(db: &DbConn) -> Result, DbErr> { + Self::install(db).await?; + seaql_migrations::Entity::find() + .order_by_asc(seaql_migrations::Column::Version) + .all(db) + .await + } + + /// Get list of migrations with status + async fn get_migration_with_status(db: &DbConn) -> Result, DbErr> { + Self::install(db).await?; + let mut migration_files = Self::get_migration_files(); + let migration_models = Self::get_migration_models(db).await?; + + let migration_in_db: HashSet = migration_models + .into_iter() + .map(|model| model.version) + .collect(); + let migration_in_fs: HashSet = migration_files + .iter() + .map(|file| file.migration.name().to_string()) + .collect(); + + let pending_migrations = &migration_in_fs - &migration_in_db; + for migration_file in migration_files.iter_mut() { + if !pending_migrations.contains(migration_file.migration.name()) { + migration_file.status = MigrationStatus::Applied; + } + } + /* + let missing_migrations_in_fs = &migration_in_db - &migration_in_fs; + let errors: Vec = missing_migrations_in_fs + .iter() + .map(|missing_migration| { + format!("Migration file of version '{}' is missing, this migration has been applied but its file is missing", missing_migration) + }).collect(); + + if !errors.is_empty() { + Err(DbErr::Custom(errors.join("\n"))) + } else { */ + Ok(migration_files) + /* } */ + } + + /// Get list of pending migrations + async fn get_pending_migrations(db: &DbConn) -> Result, DbErr> { + Self::install(db).await?; + Ok(Self::get_migration_with_status(db) + .await? + .into_iter() + .filter(|file| file.status == MigrationStatus::Pending) + .collect()) + } + + /// Get list of applied migrations + async fn get_applied_migrations(db: &DbConn) -> Result, DbErr> { + Self::install(db).await?; + Ok(Self::get_migration_with_status(db) + .await? + .into_iter() + .filter(|file| file.status == MigrationStatus::Applied) + .collect()) + } + + /// Create migration table `seaql_migrations` in the database + async fn install(db: &DbConn) -> Result<(), DbErr> { + let builder = db.get_database_backend(); + let schema = Schema::new(builder); + let mut stmt = schema.create_table_from_entity(seaql_migrations::Entity); + stmt.if_not_exists(); + db.execute(builder.build(&stmt)).await.map(|_| ()) + } + + /// Drop all tables from the database, then reapply all migrations + async fn fresh(db: &DbConn) -> Result<(), DbErr> { + Self::install(db).await?; + let db_backend = db.get_database_backend(); + + // Temporarily disable the foreign key check + if db_backend == DbBackend::Sqlite { + info!("Disabling foreign key check"); + db.execute(Statement::from_string( + db_backend, + "PRAGMA foreign_keys = OFF".to_owned(), + )) + .await?; + info!("Foreign key check disabled"); + } + + // Drop all foreign keys + if db_backend == DbBackend::MySql { + info!("Dropping all foreign keys"); + let mut stmt = Query::select(); + stmt.columns([Alias::new("TABLE_NAME"), Alias::new("CONSTRAINT_NAME")]) + .from(( + Alias::new("information_schema"), + Alias::new("table_constraints"), + )) + .cond_where( + Condition::all() + .add( + Expr::expr(get_current_schema(db)).equals( + Alias::new("table_constraints"), + Alias::new("table_schema"), + ), + ) + .add(Expr::expr(Expr::value("FOREIGN KEY")).equals( + Alias::new("table_constraints"), + Alias::new("constraint_type"), + )), + ); + let rows = db.query_all(db_backend.build(&stmt)).await?; + for row in rows.into_iter() { + let constraint_name: String = row.try_get("", "CONSTRAINT_NAME")?; + let table_name: String = row.try_get("", "TABLE_NAME")?; + info!( + "Dropping foreign key '{}' from table '{}'", + constraint_name, table_name + ); + let mut stmt = ForeignKey::drop(); + stmt.table(Alias::new(table_name.as_str())) + .name(constraint_name.as_str()); + db.execute(db_backend.build(&stmt)).await?; + info!("Foreign key '{}' has been dropped", constraint_name); + } + info!("All foreign keys dropped"); + } + + // Drop all tables + let stmt = query_tables(db); + let rows = db.query_all(db_backend.build(&stmt)).await?; + for row in rows.into_iter() { + let table_name: String = row.try_get("", "table_name")?; + info!("Dropping table '{}'", table_name); + let mut stmt = Table::drop(); + stmt.table(Alias::new(table_name.as_str())) + .if_exists() + .cascade(); + db.execute(db_backend.build(&stmt)).await?; + info!("Table '{}' has been dropped", table_name); + } + + // Restore the foreign key check + if db_backend == DbBackend::Sqlite { + info!("Restoring foreign key check"); + db.execute(Statement::from_string( + db_backend, + "PRAGMA foreign_keys = ON".to_owned(), + )) + .await?; + info!("Foreign key check restored"); + } + + // Reapply all migrations + Self::up(db, None).await + } + + /// Rollback all applied migrations, then reapply all migrations + async fn refresh(db: &DbConn) -> Result<(), DbErr> { + Self::down(db, None).await?; + Self::up(db, None).await + } + + /// Rollback all applied migrations + async fn reset(db: &DbConn) -> Result<(), DbErr> { + Self::down(db, None).await + } + + /// Check the status of all migrations + async fn status(db: &DbConn) -> Result<(), DbErr> { + Self::install(db).await?; + + info!("Checking migration status"); + + for Migration { migration, status } in Self::get_migration_with_status(db).await? { + info!("Migration '{}'... {}", migration.name(), status); + } + + Ok(()) + } + + /// Apply pending migrations + async fn up(db: &DbConn, mut steps: Option) -> Result<(), DbErr> { + Self::install(db).await?; + let manager = SchemaManager::new(db); + + if let Some(steps) = steps { + info!("Applying {} pending migrations", steps); + } else { + info!("Applying all pending migrations"); + } + + let migrations = Self::get_pending_migrations(db).await?.into_iter(); + if migrations.len() == 0 { + info!("No pending migrations"); + } + for Migration { migration, .. } in migrations { + if let Some(steps) = steps.as_mut() { + if steps == &0 { + break; + } + *steps -= 1; + } + info!("Applying migration '{}'", migration.name()); + migration.up(&manager).await?; + info!("Migration '{}' has been applied", migration.name()); + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("SystemTime before UNIX EPOCH!"); + seaql_migrations::ActiveModel { + version: ActiveValue::Set(migration.name().to_owned()), + applied_at: ActiveValue::Set(now.as_secs() as i64), + } + .insert(db) + .await?; + } + + Ok(()) + } + + /// Rollback applied migrations + async fn down(db: &DbConn, mut steps: Option) -> Result<(), DbErr> { + Self::install(db).await?; + let manager = SchemaManager::new(db); + + if let Some(steps) = steps { + info!("Rolling back {} applied migrations", steps); + } else { + info!("Rolling back all applied migrations"); + } + + let migrations = Self::get_applied_migrations(db).await?.into_iter().rev(); + if migrations.len() == 0 { + info!("No applied migrations"); + } + for Migration { migration, .. } in migrations { + if let Some(steps) = steps.as_mut() { + if steps == &0 { + break; + } + *steps -= 1; + } + info!("Rolling back migration '{}'", migration.name()); + migration.down(&manager).await?; + info!("Migration '{}' has been rollbacked", migration.name()); + seaql_migrations::Entity::delete_many() + .filter(seaql_migrations::Column::Version.eq(migration.name())) + .exec(db) + .await?; + } + + Ok(()) + } +} + +pub(crate) fn query_tables(db: &DbConn) -> SelectStatement { + match db.get_database_backend() { + DbBackend::MySql => MySql::query_tables(), + DbBackend::Postgres => Postgres::query_tables(), + DbBackend::Sqlite => Sqlite::query_tables(), + } +} + +pub(crate) fn get_current_schema(db: &DbConn) -> SimpleExpr { + match db.get_database_backend() { + DbBackend::MySql => MySql::get_current_schema(), + DbBackend::Postgres => Postgres::get_current_schema(), + DbBackend::Sqlite => unimplemented!(), + } +} diff --git a/pagetop/src/db/migration/prelude.rs b/pagetop/src/db/migration/prelude.rs new file mode 100644 index 00000000..5d1e6f49 --- /dev/null +++ b/pagetop/src/db/migration/prelude.rs @@ -0,0 +1,10 @@ +//pub use super::cli; +pub use super::manager::SchemaManager; +pub use super::migrator::MigratorTrait; +pub use super::{MigrationName, MigrationTrait}; +pub use async_trait; +pub use sea_orm; +pub use sea_orm::sea_query; +pub use sea_orm::sea_query::*; +pub use sea_orm::DbErr; +pub use sea_orm::DeriveMigrationName; diff --git a/pagetop/src/db/migration/seaql_migrations.rs b/pagetop/src/db/migration/seaql_migrations.rs new file mode 100644 index 00000000..43f044ec --- /dev/null +++ b/pagetop/src/db/migration/seaql_migrations.rs @@ -0,0 +1,14 @@ +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "seaql_migrations")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub version: String, + pub applied_at: i64, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/pagetop/src/html/favicon.rs b/pagetop/src/html/favicon.rs index 45324b92..ac669ef8 100644 --- a/pagetop/src/html/favicon.rs +++ b/pagetop/src/html/favicon.rs @@ -31,17 +31,23 @@ impl Favicon { } pub fn with_theme_color(mut self, color: &str) -> Self { - self.0.push(html! { meta name="theme-color" content=(color); }); + self.0.push(html! { + meta name="theme-color" content=(color); + }); self } pub fn with_ms_tile_color(mut self, color: &str) -> Self { - self.0.push(html! { meta name="msapplication-TileColor" content=(color); }); + self.0.push(html! { + meta name="msapplication-TileColor" content=(color); + }); self } pub fn with_ms_tile_image(mut self, image: &str) -> Self { - self.0.push(html! { meta name="msapplication-TileImage" content=(image); }); + self.0.push(html! { + meta name="msapplication-TileImage" content=(image); + }); self } diff --git a/pagetop/src/response/page/context.rs b/pagetop/src/response/page/context.rs index cb8571d7..055fa781 100644 --- a/pagetop/src/response/page/context.rs +++ b/pagetop/src/response/page/context.rs @@ -1,11 +1,10 @@ use super::PageOp; use crate::config::SETTINGS; -use crate::core::theme::all::theme_by_single_name; -use crate::core::theme::ThemeTrait; +use crate::core::theme::{all::theme_by_single_name, ThemeStaticRef}; use crate::html::{html, Assets, Favicon, IdentifierValue, JavaScript, Markup, ModeJS, StyleSheet}; use crate::{base, concat_string, util, LazyStatic}; -static DEFAULT_THEME: LazyStatic<&dyn ThemeTrait> = +static DEFAULT_THEME: LazyStatic = LazyStatic::new(|| match theme_by_single_name(&SETTINGS.app.theme) { Some(theme) => theme, None => &base::theme::bootsier::Bootsier, @@ -13,7 +12,7 @@ static DEFAULT_THEME: LazyStatic<&dyn ThemeTrait> = #[rustfmt::skip] pub struct PageContext { - theme : &'static dyn ThemeTrait, + theme : ThemeStaticRef, favicon : Option, metadata : Vec<(&'static str, &'static str)>, properties : Vec<(&'static str, &'static str)>, @@ -94,7 +93,7 @@ impl PageContext { /// PageContext GETTERS. - pub(crate) fn theme(&mut self) -> &'static dyn ThemeTrait { + pub(crate) fn theme(&mut self) -> ThemeStaticRef { self.theme } diff --git a/website/src/main.rs b/website/src/main.rs index 407284da..80604748 100644 --- a/website/src/main.rs +++ b/website/src/main.rs @@ -5,7 +5,7 @@ mod mdbook; struct PageTopWebSite; impl AppTrait for PageTopWebSite { - fn enable_modules(&self) -> Vec<&'static dyn ModuleTrait> { + fn enable_modules(&self) -> Vec { vec![&mdbook::MdBook] } }