WIP: Añade nueva extensión para dar soporte a bases de datos #12
72 changed files with 4495 additions and 422 deletions
|
|
@ -1,3 +1,4 @@
|
|||
[alias]
|
||||
ts = ["test", "--features", "testing"] # cargo ts
|
||||
tw = ["test", "--workspace", "--features", "testing"] # cargo tw
|
||||
td = ["test", "--doc", "-p"] # cargo td <crate>
|
||||
|
|
|
|||
|
|
@ -26,13 +26,6 @@ para mostrar un banner de presentación en el terminal con el nombre de la aplic
|
|||
* [starwars.flf](http://www.figlet.org/fontdb_example.cgi?font=starwars.flf) de *Ryan Youck*
|
||||
|
||||
|
||||
# 🎨 CSS
|
||||
|
||||
La extensión `pagetop-bootsier` es un tema que integra [Bootstrap 5.3.8](https://getbootstrap.com/)
|
||||
para los estilos y componentes de la interfaz. Bootstrap está distribuido bajo licencia
|
||||
[MIT](https://github.com/twbs/bootstrap/blob/main/LICENSE).
|
||||
|
||||
|
||||
# 👾 Icono
|
||||
|
||||
"La Mascota" sonriente es una simpática creación de [Webalys](https://www.iconfinder.com/webalys).
|
||||
|
|
|
|||
1628
Cargo.lock
generated
1628
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -69,6 +69,7 @@ members = [
|
|||
# Extensions
|
||||
"extensions/pagetop-aliner",
|
||||
"extensions/pagetop-bootsier",
|
||||
"extensions/pagetop-seaorm",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
|
@ -88,5 +89,6 @@ pagetop-statics = { version = "0.1", path = "helpers/pagetop-statics" }
|
|||
# Extensions
|
||||
pagetop-aliner = { version = "0.1", path = "extensions/pagetop-aliner" }
|
||||
pagetop-bootsier = { version = "0.1", path = "extensions/pagetop-bootsier" }
|
||||
pagetop-seaorm = { version = "0.0", path = "extensions/pagetop-seaorm" }
|
||||
# PageTop
|
||||
pagetop = { version = "0.5", path = "." }
|
||||
|
|
|
|||
24
README.md
24
README.md
|
|
@ -11,9 +11,10 @@
|
|||
[](https://crates.io/crates/pagetop)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop#licencia)
|
||||
|
||||
<br>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
PageTop reivindica la esencia de la web clásica usando [Rust](https://www.rust-lang.org/es) para la
|
||||
creación de soluciones web SSR (*renderizadas en el servidor*) basadas en HTML, CSS y JavaScript.
|
||||
Ofrece un conjunto de herramientas que los desarrolladores pueden implementar, extender o adaptar
|
||||
|
|
@ -109,6 +110,9 @@ El código se organiza en un *workspace* donde actualmente se incluyen los sigui
|
|||
tema basado en [Bootstrap](https://getbootstrap.com) para integrar su catálogo de estilos y
|
||||
componentes flexibles.
|
||||
|
||||
* **[pagetop-seaorm](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-seaorm)**,
|
||||
integra [SeaORM](https://www.sea-ql.org/SeaORM) para acceder a bases de datos relacionales.
|
||||
|
||||
|
||||
## 🧪 Pruebas
|
||||
|
||||
|
|
@ -116,15 +120,19 @@ Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de C
|
|||
`.cargo/config.toml`. Basta con ejecutarlos desde la raíz del proyecto:
|
||||
|
||||
| Comando | Descripción |
|
||||
| ------- | ----------- |
|
||||
| `cargo ts` | Ejecuta los tests de `pagetop` (*unit + integration*) con la *feature* `testing`. |
|
||||
| `cargo ts --test util` | Lanza sólo las pruebas de integración del módulo `util`. |
|
||||
| `cargo ts --doc locale` | Lanza las pruebas de la documentación del módulo `locale`. |
|
||||
| `cargo tw` | Ejecuta los tests de **todos los paquetes** del *workspace*. |
|
||||
| ----------------------- | --------------------------------------------------------------- |
|
||||
| `cargo ts` | Lanza **todos los tests** de `pagetop` |
|
||||
| `cargo ts --test util` | Lanza los tests de integración del archivo `tests/util.rs` |
|
||||
| `cargo ts --doc locale` | Lanza los *doctests* de `pagetop` cuyo *path* contiene `locale` |
|
||||
| `cargo tw` | Lanza **todos los tests** del *workspace* |
|
||||
| `cargo td <crate>` | Lanza los *doctests* de un *crate* concreto del *workspace* |
|
||||
|
||||
> **Nota**
|
||||
> Estos alias ya compilan con la configuración adecuada. No requieren `--no-default-features`.
|
||||
> Si quieres **activar** las trazas del registro de eventos entonces usa simplemente `cargo test`.
|
||||
> * Todos los alias, excepto `cargo td`, aplican la *feature* `testing` para los *crates* que la
|
||||
> declaren.
|
||||
> * Cuando lanza **todos los tests** se incluyen las pruebas unitarias, de integración y *doctests*.
|
||||
> * Los alias suprimen las trazas del registro de eventos. Para activarlas usa directamente
|
||||
> `cargo test`.
|
||||
|
||||
|
||||
## 🚧 Advertencia
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use pagetop_bootsier::prelude::*;
|
||||
use pagetop_bootsier::theme::*;
|
||||
|
||||
include_locales!(LOC from "examples/locale");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use pagetop_bootsier::prelude::*;
|
||||
use pagetop_bootsier::theme::*;
|
||||
|
||||
include_locales!(LOC from "examples/locale");
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
[](https://crates.io/crates/pagetop-aliner)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-aliner#licencia)
|
||||
|
||||
<br>
|
||||
</div>
|
||||
|
||||
## 🧭 Sobre PageTop
|
||||
|
|
|
|||
|
|
@ -83,9 +83,12 @@ async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
|||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
/// Implementa el tema para usar en pruebas que muestran el esquema de páginas HTML.
|
||||
include_locales!(LOCALES_ALINER);
|
||||
|
||||
/// Implementa el tema.
|
||||
///
|
||||
/// Define un tema mínimo útil para:
|
||||
/// Define un tema mínimo que muestra esquemáticamente la composición de las páginas HTML; útil
|
||||
/// para:
|
||||
///
|
||||
/// - Comprobar el funcionamiento de temas, plantillas y regiones.
|
||||
/// - Verificar integración de componentes y composiciones (*layouts*) sin estilos complejos.
|
||||
|
|
@ -94,6 +97,14 @@ use pagetop::prelude::*;
|
|||
pub struct Aliner;
|
||||
|
||||
impl Extension for Aliner {
|
||||
fn name(&self) -> L10n {
|
||||
L10n::t("extension_name", &LOCALES_ALINER)
|
||||
}
|
||||
|
||||
fn description(&self) -> L10n {
|
||||
L10n::t("extension_description", &LOCALES_ALINER)
|
||||
}
|
||||
|
||||
fn theme(&self) -> Option<ThemeRef> {
|
||||
Some(&Self)
|
||||
}
|
||||
|
|
|
|||
2
extensions/pagetop-aliner/src/locale/en-US/extension.ftl
Normal file
2
extensions/pagetop-aliner/src/locale/en-US/extension.ftl
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
extension_name = Aliner
|
||||
extension_description = Minimal theme that schematically shows the HTML page composition.
|
||||
2
extensions/pagetop-aliner/src/locale/es-ES/extension.ftl
Normal file
2
extensions/pagetop-aliner/src/locale/es-ES/extension.ftl
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
extension_name = Aliner
|
||||
extension_description = Tema mínimo que muestra esquemáticamente la composición de las páginas HTML.
|
||||
|
|
@ -9,7 +9,6 @@
|
|||
[](https://crates.io/crates/pagetop-bootsier)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-bootsier#licencia)
|
||||
|
||||
<br>
|
||||
</div>
|
||||
|
||||
## 🧭 Sobre PageTop
|
||||
|
|
@ -80,6 +79,13 @@ async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
|||
```
|
||||
|
||||
|
||||
## 📚 Créditos
|
||||
|
||||
Este *crate* integra la biblioteca de estilos [Bootstrap 5.3.8](https://getbootstrap.com/) para
|
||||
definir el comportamiento, la apariencia y los componentes de la interfaz. Bootstrap se distribuye
|
||||
bajo licencia [MIT](https://github.com/twbs/bootstrap/blob/main/LICENSE).
|
||||
|
||||
|
||||
## 🚧 Advertencia
|
||||
|
||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||
|
|
|
|||
|
|
@ -96,12 +96,6 @@ pub mod config;
|
|||
|
||||
pub mod theme;
|
||||
|
||||
/// *Prelude* del tema.
|
||||
pub mod prelude {
|
||||
pub use crate::config::*;
|
||||
pub use crate::theme::*;
|
||||
}
|
||||
|
||||
/// Plantillas que Bootsier añade.
|
||||
#[derive(AutoDefault)]
|
||||
pub enum BootsierTemplate {
|
||||
|
|
@ -134,6 +128,14 @@ impl Template for BootsierTemplate {
|
|||
pub struct Bootsier;
|
||||
|
||||
impl Extension for Bootsier {
|
||||
fn name(&self) -> L10n {
|
||||
L10n::t("extension_name", &LOCALES_BOOTSIER)
|
||||
}
|
||||
|
||||
fn description(&self) -> L10n {
|
||||
L10n::t("extension_description", &LOCALES_BOOTSIER)
|
||||
}
|
||||
|
||||
fn theme(&self) -> Option<ThemeRef> {
|
||||
Some(&Self)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
extension_name = Bootsier
|
||||
extension_description = Bootstrap-based theme with flexible styles and components.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
extension_name = Bootsier
|
||||
extension_description = Tema basado en Bootstrap para aplicar su catálogo de estilos y componentes flexibles.
|
||||
|
|
@ -58,7 +58,7 @@ impl BorderColor {
|
|||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// assert_eq!(BorderColor::Theme(Color::Primary).to_class(), "border-primary");
|
||||
/// assert_eq!(BorderColor::Subtle(Color::Warning).to_class(), "border-warning-subtle");
|
||||
/// assert_eq!(BorderColor::Black.to_class(), "border-black");
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ impl BreakPoint {
|
|||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let bp = BreakPoint::MD;
|
||||
/// assert_eq!(bp.class_with("col", ""), "col-md");
|
||||
/// assert_eq!(bp.class_with("col", "6"), "col-md-6");
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ impl ButtonColor {
|
|||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// assert_eq!(
|
||||
/// ButtonColor::Background(Color::Primary).to_class(),
|
||||
/// "btn-primary"
|
||||
|
|
@ -132,7 +132,7 @@ impl ButtonSize {
|
|||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// assert_eq!(ButtonSize::Small.to_class(), "btn-sm");
|
||||
/// assert_eq!(ButtonSize::Large.to_class(), "btn-lg");
|
||||
/// assert_eq!(ButtonSize::Default.to_class(), "");
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ impl Color {
|
|||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// assert_eq!(Color::Primary.to_class(), "primary");
|
||||
/// assert_eq!(Color::Danger.to_class(), "danger");
|
||||
/// ```
|
||||
|
|
@ -124,7 +124,7 @@ impl Opacity {
|
|||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// assert_eq!(Opacity::Opaque.class_with(""), "opacity-100");
|
||||
/// assert_eq!(Opacity::Half.class_with("bg"), "bg-opacity-50");
|
||||
/// assert_eq!(Opacity::SemiTransparent.class_with("text"), "text-opacity-25");
|
||||
|
|
@ -156,7 +156,7 @@ impl Opacity {
|
|||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// assert_eq!(Opacity::Opaque.to_class(), "opacity-100");
|
||||
/// assert_eq!(Opacity::Half.to_class(), "opacity-50");
|
||||
/// assert_eq!(Opacity::Default.to_class(), "");
|
||||
|
|
@ -237,7 +237,7 @@ impl ColorBg {
|
|||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// assert_eq!(ColorBg::Body.to_class(), "bg-body");
|
||||
/// assert_eq!(ColorBg::Theme(Color::Primary).to_class(), "bg-primary");
|
||||
/// assert_eq!(ColorBg::Subtle(Color::Warning).to_class(), "bg-warning-subtle");
|
||||
|
|
@ -321,7 +321,7 @@ impl ColorText {
|
|||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// assert_eq!(ColorText::Body.to_class(), "text-body");
|
||||
/// assert_eq!(ColorText::Theme(Color::Primary).to_class(), "text-primary");
|
||||
/// assert_eq!(ColorText::Emphasis(Color::Danger).to_class(), "text-danger-emphasis");
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ impl ScaleSize {
|
|||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// assert_eq!(ScaleSize::Auto.class_with("border"), "border");
|
||||
/// assert_eq!(ScaleSize::Zero.class_with("m"), "m-0");
|
||||
/// assert_eq!(ScaleSize::Three.class_with("p"), "p-3");
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ impl RoundedRadius {
|
|||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// assert_eq!(RoundedRadius::Scale2.class_with(""), "rounded-2");
|
||||
/// assert_eq!(RoundedRadius::Zero.class_with("rounded-top"), "rounded-top-0");
|
||||
/// assert_eq!(RoundedRadius::Scale3.class_with("rounded-top-end"), "rounded-top-end-3");
|
||||
|
|
@ -103,7 +103,7 @@ impl RoundedRadius {
|
|||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// assert_eq!(RoundedRadius::Default.to_class(), "rounded");
|
||||
/// assert_eq!(RoundedRadius::Zero.to_class(), "rounded-0");
|
||||
/// assert_eq!(RoundedRadius::Scale3.to_class(), "rounded-3");
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ use crate::theme::{ButtonAction, ButtonColor, ButtonSize};
|
|||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_bootsier::theme::*;
|
||||
///
|
||||
/// let save = Button::submit(L10n::n("Save"))
|
||||
/// .with_color(ButtonColor::Background(Color::Primary));
|
||||
///
|
||||
|
|
|
|||
|
|
@ -26,45 +26,33 @@ use crate::theme::attrs::{BorderColor, Opacity, ScaleSize, Side};
|
|||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// **Borde global:**
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// use pagetop_bootsier::theme::*;
|
||||
///
|
||||
/// // Borde global.
|
||||
/// let b = classes::Border::with(ScaleSize::Two);
|
||||
/// assert_eq!(b.to_class(), "border-2");
|
||||
/// ```
|
||||
///
|
||||
/// **Aditivo (solo borde superior):**
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// // Aditivo (sólo borde superior):
|
||||
/// let b = classes::Border::default().with_side(Side::Top, ScaleSize::One);
|
||||
/// assert_eq!(b.to_class(), "border-top-1");
|
||||
/// ```
|
||||
///
|
||||
/// **Sustractivo (borde global menos el superior):**
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// // Sustractivo (borde global menos el superior):
|
||||
/// let b = classes::Border::new().with_side(Side::Top, ScaleSize::Zero);
|
||||
/// assert_eq!(b.to_class(), "border border-top-0");
|
||||
/// ```
|
||||
///
|
||||
/// **Ancho por lado (lado lógico inicial a 2 y final a 4):**
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// // Ancho por lado (lado lógico inicial a 2 y final a 4):
|
||||
/// let b = classes::Border::default()
|
||||
/// .with_side(Side::Start, ScaleSize::Two)
|
||||
/// .with_side(Side::End, ScaleSize::Four);
|
||||
/// assert_eq!(b.to_class(), "border-end-4 border-start-2");
|
||||
/// ```
|
||||
///
|
||||
/// **Combinado (ejemplo completo):**
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// // Combinado (ejemplo completo):
|
||||
/// let b = classes::Border::new() // Borde por defecto.
|
||||
/// .with_side(Side::Top, ScaleSize::Zero) // Quita borde superior.
|
||||
/// .with_side(Side::End, ScaleSize::Three) // Ancho 3 para el lado lógico final.
|
||||
/// .with_color(BorderColor::Theme(Color::Primary))
|
||||
/// .with_opacity(Opacity::Half);
|
||||
///
|
||||
/// assert_eq!(b.to_class(), "border border-top-0 border-end-3 border-primary border-opacity-50");
|
||||
/// ```
|
||||
#[rustfmt::skip]
|
||||
|
|
@ -158,7 +146,7 @@ impl Border {
|
|||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// // Convertir explícitamente con `From::from`:
|
||||
/// let b = classes::Border::from(ScaleSize::Two);
|
||||
/// assert_eq!(b.to_class(), "border-2");
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ use crate::theme::attrs::{ColorBg, ColorText, Opacity};
|
|||
/// # Ejemplos
|
||||
///
|
||||
/// ```
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// use pagetop_bootsier::theme::*;
|
||||
///
|
||||
/// // Sin clases.
|
||||
/// let s = classes::Background::new();
|
||||
/// assert_eq!(s.to_class(), "");
|
||||
|
|
@ -90,7 +91,7 @@ impl From<(ColorBg, Opacity)> for Background {
|
|||
/// # Ejemplo
|
||||
///
|
||||
/// ```
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let s: classes::Background = (ColorBg::White, Opacity::SemiTransparent).into();
|
||||
/// assert_eq!(s.to_class(), "bg-white bg-opacity-25");
|
||||
/// ```
|
||||
|
|
@ -105,7 +106,7 @@ impl From<ColorBg> for Background {
|
|||
/// # Ejemplo
|
||||
///
|
||||
/// ```
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let s: classes::Background = ColorBg::Black.into();
|
||||
/// assert_eq!(s.to_class(), "bg-black");
|
||||
/// ```
|
||||
|
|
@ -121,7 +122,8 @@ impl From<ColorBg> for Background {
|
|||
/// # Ejemplos
|
||||
///
|
||||
/// ```
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// use pagetop_bootsier::theme::*;
|
||||
///
|
||||
/// // Sin clases.
|
||||
/// let s = classes::Text::new();
|
||||
/// assert_eq!(s.to_class(), "");
|
||||
|
|
@ -202,7 +204,7 @@ impl From<(ColorText, Opacity)> for Text {
|
|||
/// # Ejemplo
|
||||
///
|
||||
/// ```
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let s: classes::Text = (ColorText::Theme(Color::Danger), Opacity::Opaque).into();
|
||||
/// assert_eq!(s.to_class(), "text-danger text-opacity-100");
|
||||
/// ```
|
||||
|
|
@ -218,7 +220,7 @@ impl From<ColorText> for Text {
|
|||
/// # Ejemplo
|
||||
///
|
||||
/// ```
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let s: classes::Text = ColorText::Black.into();
|
||||
/// assert_eq!(s.to_class(), "text-black");
|
||||
/// ```
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ use crate::theme::BreakPoint;
|
|||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// use pagetop_bootsier::theme::*;
|
||||
///
|
||||
/// let m = classes::Margin::with(Side::Top, ScaleSize::Three);
|
||||
/// assert_eq!(m.to_class(), "mt-3");
|
||||
///
|
||||
|
|
@ -97,7 +98,8 @@ impl Margin {
|
|||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// use pagetop_bootsier::theme::*;
|
||||
///
|
||||
/// let p = classes::Padding::with(Side::LeftAndRight, ScaleSize::Two);
|
||||
/// assert_eq!(p.to_class(), "px-2");
|
||||
///
|
||||
|
|
|
|||
|
|
@ -14,42 +14,30 @@ use crate::theme::attrs::RoundedRadius;
|
|||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// **Radio global:**
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// use pagetop_bootsier::theme::*;
|
||||
///
|
||||
/// // Radio global:
|
||||
/// let r = classes::Rounded::with(RoundedRadius::Default);
|
||||
/// assert_eq!(r.to_class(), "rounded");
|
||||
/// ```
|
||||
///
|
||||
/// **Sin redondeo:**
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// // Sin redondeo:
|
||||
/// let r = classes::Rounded::new();
|
||||
/// assert_eq!(r.to_class(), "");
|
||||
/// ```
|
||||
///
|
||||
/// **Radio en las esquinas de un lado lógico:**
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// // Radio en las esquinas de un lado lógico:
|
||||
/// let r = classes::Rounded::new().with_end(RoundedRadius::Scale2);
|
||||
/// assert_eq!(r.to_class(), "rounded-end-2");
|
||||
/// ```
|
||||
///
|
||||
/// **Radio en una esquina concreta:**
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// // Radio en una esquina concreta:
|
||||
/// let r = classes::Rounded::new().with_top_start(RoundedRadius::Scale3);
|
||||
/// assert_eq!(r.to_class(), "rounded-top-start-3");
|
||||
/// ```
|
||||
///
|
||||
/// **Combinado (ejemplo completo):**
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// // Combinado (ejemplo completo):
|
||||
/// let r = classes::Rounded::new()
|
||||
/// .with_top(RoundedRadius::Default) // Añade redondeo arriba.
|
||||
/// .with_bottom_start(RoundedRadius::Scale4) // Añade una esquina redondeada concreta.
|
||||
/// .with_bottom_end(RoundedRadius::Circle); // Añade redondeo extremo en otra esquina.
|
||||
///
|
||||
/// assert_eq!(r.to_class(), "rounded-top rounded-bottom-start-4 rounded-bottom-end-circle");
|
||||
/// ```
|
||||
#[rustfmt::skip]
|
||||
|
|
|
|||
|
|
@ -6,16 +6,6 @@
|
|||
//! Con [`container::Width`](crate::theme::container::Width) se puede definir el ancho y el
|
||||
//! comportamiento *responsive* del contenedor. También permite aplicar utilidades de estilo para el
|
||||
//! fondo, texto, borde o esquinas redondeadas.
|
||||
//!
|
||||
//! # Ejemplo
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let main = Container::main()
|
||||
//! .with_id("main-page")
|
||||
//! .with_width(container::Width::From(BreakPoint::LG));
|
||||
//! ```
|
||||
|
||||
mod props;
|
||||
pub use props::{Kind, Width};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,23 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::theme::*;
|
||||
|
||||
/// Componente para crear un **contenedor de componentes**.
|
||||
/// Componente para crear un **contenedor de componentes** ([`container`]).
|
||||
///
|
||||
/// Envuelve un contenido con la etiqueta HTML indicada por [`container::Kind`]. Sólo se renderiza
|
||||
/// si existen componentes hijos (*children*).
|
||||
/// Envuelve un conjunto de componentes en un contenedor establecido que se crea aplicando uno de
|
||||
/// los tipos definidos en [`container::Kind`].
|
||||
///
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// use pagetop_bootsier::theme::*;
|
||||
///
|
||||
/// let main = Container::main()
|
||||
/// .with_id("main-page")
|
||||
/// .with_width(container::Width::From(BreakPoint::LG));
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Container {
|
||||
#[getters(skip)]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Definiciones para crear menús desplegables [`Dropdown`].
|
||||
//! Definiciones para crear menús desplegables ([`Dropdown`]).
|
||||
//!
|
||||
//! Cada [`dropdown::Item`](crate::theme::dropdown::Item) representa un elemento individual del
|
||||
//! desplegable [`Dropdown`], con distintos comportamientos según su finalidad, como enlaces de
|
||||
|
|
@ -6,23 +6,6 @@
|
|||
//!
|
||||
//! Los ítems pueden estar activos, deshabilitados o abrirse en nueva ventana según su contexto y
|
||||
//! configuración, y permiten incluir etiquetas localizables usando [`L10n`](pagetop::locale::L10n).
|
||||
//!
|
||||
//! # Ejemplo
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let dd = Dropdown::new()
|
||||
//! .with_title(L10n::n("Menu"))
|
||||
//! .with_button_color(ButtonColor::Background(Color::Secondary))
|
||||
//! .with_auto_close(dropdown::AutoClose::ClickableInside)
|
||||
//! .with_direction(dropdown::Direction::Dropend)
|
||||
//! .with_item(dropdown::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
//! .with_item(dropdown::Item::link_blank(L10n::n("External"), |_| "https://docs.rs".into()))
|
||||
//! .with_item(dropdown::Item::divider())
|
||||
//! .with_item(dropdown::Item::header(L10n::n("User session")))
|
||||
//! .with_item(dropdown::Item::button(L10n::n("Sign out")));
|
||||
//! ```
|
||||
|
||||
mod props;
|
||||
pub use props::{AutoClose, Direction, MenuAlign, MenuPosition};
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::theme::*;
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
/// Componente para crear un **menú desplegable**.
|
||||
/// Componente para crear un **menú desplegable** ([`dropdown`]).
|
||||
///
|
||||
/// Renderiza un botón (único o desdoblado, ver [`with_button_split()`](Self::with_button_split))
|
||||
/// para mostrar un menú desplegable de elementos [`dropdown::Item`], que se muestra/oculta según la
|
||||
/// interacción del usuario. Admite variaciones de tamaño/color del botón, también dirección de
|
||||
/// apertura, alineación o política de cierre.
|
||||
/// para mostrar un menú desplegable de elementos [`dropdown::Item`], que se muestra u oculta según
|
||||
/// la interacción del usuario. Admite variaciones para el tamaño y el color del botón, también para
|
||||
/// la dirección de apertura, alineación o política de cierre.
|
||||
///
|
||||
/// Si no tiene título (ver [`with_title()`](Self::with_title)) se muestra únicamente la lista de
|
||||
/// elementos sin ningún botón para interactuar.
|
||||
|
|
@ -17,8 +17,25 @@ use crate::LOCALES_BOOTSIER;
|
|||
/// cuenta **el título** (si no existe le asigna uno por defecto) y **la lista de elementos**; el
|
||||
/// resto de propiedades no afectarán a su representación en [`Nav`].
|
||||
///
|
||||
/// Ver ejemplo en el módulo [`dropdown`].
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_bootsier::theme::*;
|
||||
///
|
||||
/// let dd = Dropdown::new()
|
||||
/// .with_title(L10n::n("Menu"))
|
||||
/// .with_button_color(ButtonColor::Background(Color::Secondary))
|
||||
/// .with_auto_close(dropdown::AutoClose::ClickableInside)
|
||||
/// .with_direction(dropdown::Direction::Dropend)
|
||||
/// .with_item(dropdown::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
/// .with_item(dropdown::Item::link_blank(L10n::n("External"), |_| "https://docs.rs".into()))
|
||||
/// .with_item(dropdown::Item::divider())
|
||||
/// .with_item(dropdown::Item::header(L10n::n("User session")))
|
||||
/// .with_item(dropdown::Item::button(L10n::n("Sign out")));
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Dropdown {
|
||||
#[getters(skip)]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::theme::*;
|
||||
|
||||
// **< AutoClose >**********************************************************************************
|
||||
|
||||
|
|
|
|||
|
|
@ -1,36 +1,4 @@
|
|||
//! Definiciones para crear formularios ([`Form`]).
|
||||
//!
|
||||
//! # Ejemplo
|
||||
//!
|
||||
//! ```rust
|
||||
//! use pagetop::prelude::*;
|
||||
//! use pagetop_bootsier::prelude::*;
|
||||
//!
|
||||
//! let form_login = Form::new()
|
||||
//! .with_id("login")
|
||||
//! .with_action("/login")
|
||||
//! .with_child(
|
||||
//! form::input::Field::email()
|
||||
//! .with_name("email")
|
||||
//! .with_label(L10n::n("Email"))
|
||||
//! .with_required(true),
|
||||
//! )
|
||||
//! .with_child(
|
||||
//! form::input::Field::password()
|
||||
//! .with_name("password")
|
||||
//! .with_label(L10n::n("Password"))
|
||||
//! .with_required(true),
|
||||
//! )
|
||||
//! .with_child(
|
||||
//! form::Checkbox::check()
|
||||
//! .with_name("remember")
|
||||
//! .with_label(L10n::n("Remember me")),
|
||||
//! )
|
||||
//! .with_child(
|
||||
//! Button::submit(L10n::n("Sign in"))
|
||||
//! .with_color(ButtonColor::Background(Color::Primary)),
|
||||
//! );
|
||||
//! ```
|
||||
|
||||
mod props;
|
||||
pub use props::{Autocomplete, AutofillField, CheckboxKind, Method};
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use pagetop::prelude::*;
|
|||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let item = form::check::Item::new("apple", L10n::n("Apple")).with_checked(true);
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
|
|
@ -82,7 +82,7 @@ impl Item {
|
|||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let interests = form::check::Field::new()
|
||||
/// .with_name("interests")
|
||||
/// .with_label(L10n::n("Areas of interest"))
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use crate::LOCALES_BOOTSIER;
|
|||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let accept_terms = form::Checkbox::check() // También sirve new() o default().
|
||||
/// .with_name("terms_accepted")
|
||||
/// .with_label(L10n::n("I accept the terms and conditions"))
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ use pagetop::prelude::*;
|
|||
|
||||
use crate::theme::form;
|
||||
|
||||
/// Componente para crear un **formulario**.
|
||||
/// Componente para crear un **formulario** ([`form`]).
|
||||
///
|
||||
/// Este componente renderiza un `<form>` estándar con soporte para los atributos más habituales:
|
||||
/// Este componente renderiza un formulario estándar con soporte para los atributos más habituales:
|
||||
///
|
||||
/// - `id`: identificador opcional del formulario.
|
||||
/// - `classes`: clases CSS adicionales (p. ej. utilidades CSS).
|
||||
|
|
@ -17,13 +17,33 @@ use crate::theme::form;
|
|||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let search = Form::new()
|
||||
/// .with_id("search")
|
||||
/// .with_action("/search")
|
||||
/// .with_method(form::Method::Get)
|
||||
/// .with_child(form::input::Field::search().with_name("q"));
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_bootsier::theme::*;
|
||||
///
|
||||
/// let form_login = Form::new()
|
||||
/// .with_id("login")
|
||||
/// .with_action("/login")
|
||||
/// .with_child(
|
||||
/// form::input::Field::email()
|
||||
/// .with_name("email")
|
||||
/// .with_label(L10n::n("Email"))
|
||||
/// .with_required(true),
|
||||
/// )
|
||||
/// .with_child(
|
||||
/// form::input::Field::password()
|
||||
/// .with_name("password")
|
||||
/// .with_label(L10n::n("Password"))
|
||||
/// .with_required(true),
|
||||
/// )
|
||||
/// .with_child(
|
||||
/// form::Checkbox::check()
|
||||
/// .with_name("remember")
|
||||
/// .with_label(L10n::n("Remember me")),
|
||||
/// )
|
||||
/// .with_child(
|
||||
/// Button::submit(L10n::n("Sign in"))
|
||||
/// .with_color(ButtonColor::Background(Color::Primary)),
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Form {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use pagetop::prelude::*;
|
|||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let personal_data = form::Fieldset::new()
|
||||
/// .with_legend(L10n::n("Personal data"))
|
||||
/// .with_description(L10n::n("Enter your full name and contact email."))
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use pagetop::prelude::*;
|
|||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let token = form::Hidden::new()
|
||||
/// .with_name("csrf_token")
|
||||
/// .with_value("a1b2c3d4e5");
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ impl fmt::Display for Mode {
|
|||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let email = form::input::Field::email()
|
||||
/// .with_name("email")
|
||||
/// .with_label(L10n::n("Email address"))
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ pub enum CheckboxKind {
|
|||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// // Correo electrónico con sugerencia semántica del navegador.
|
||||
/// let ac = form::Autocomplete::email();
|
||||
///
|
||||
|
|
@ -244,7 +244,7 @@ impl fmt::Display for Autocomplete {
|
|||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let ac = form::Autocomplete::token(form::AutofillField::Username);
|
||||
/// let ac = form::Autocomplete::shipping(form::AutofillField::StreetAddress);
|
||||
/// let ac = form::Autocomplete::section("job", form::AutofillField::Email);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ use crate::LOCALES_BOOTSIER;
|
|||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let item = form::radio::Item::new("monthly", L10n::n("Monthly")).with_checked(true);
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
|
|
@ -76,7 +76,7 @@ impl Item {
|
|||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let plan = form::radio::Field::new()
|
||||
/// .with_name("plan")
|
||||
/// .with_label(L10n::n("Subscription plan"))
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use pagetop::prelude::*;
|
|||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let volume = form::Range::new()
|
||||
/// .with_name("volume")
|
||||
/// .with_label(L10n::n("Volume"))
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ use crate::LOCALES_BOOTSIER;
|
|||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let item = form::select::Item::new("es", L10n::n("Spanish")).with_selected(true);
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
|
|
@ -76,7 +76,7 @@ impl Item {
|
|||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let group = form::select::Group::new(L10n::n("Europe"))
|
||||
/// .with_item(form::select::Item::new("es", L10n::n("Spanish")))
|
||||
/// .with_item(form::select::Item::new("fr", L10n::n("French")));
|
||||
|
|
@ -149,7 +149,7 @@ pub enum Entry {
|
|||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let idioma = form::select::Field::new()
|
||||
/// .with_name("language")
|
||||
/// .with_label(L10n::n("Language"))
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use crate::LOCALES_BOOTSIER;
|
|||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let descripcion = form::Textarea::new()
|
||||
/// .with_name("description")
|
||||
/// .with_label(L10n::n("Description"))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::prelude::*;
|
||||
use crate::theme::*;
|
||||
|
||||
const DEFAULT_VIEWBOX: &str = "0 0 16 16";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::theme::*;
|
||||
|
||||
/// Componente para renderizar una **imagen**.
|
||||
/// Componente para renderizar una **imagen** ([`image`]).
|
||||
///
|
||||
/// - Ajusta su disposición según el origen definido en [`image::Source`].
|
||||
/// - Permite configurar **dimensiones** ([`with_size()`](Self::with_size)), **borde**
|
||||
/// A una imagen se le puede:
|
||||
///
|
||||
/// - Establecer su contenido a partir del origen definido en [`image::Source`].
|
||||
/// - Configurar sus **dimensiones** ([`with_size()`](Self::with_size)), **borde**
|
||||
/// ([`classes::Border`](crate::theme::classes::Border)) y **redondeo de esquinas**
|
||||
/// ([`classes::Rounded`](crate::theme::classes::Rounded)).
|
||||
/// - Resuelve el texto alternativo `alt` con **localización** mediante [`L10n`].
|
||||
/// - Aplicar el texto alternativo `alt` con **localización** mediante [`L10n`].
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Image {
|
||||
#[getters(skip)]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Definiciones para crear menús [`Nav`] o alguna de sus variantes de presentación.
|
||||
//! Definiciones para crear menús ([`Nav`]).
|
||||
//!
|
||||
//! Cada [`nav::Item`](crate::theme::nav::Item) representa un elemento individual del menú [`Nav`],
|
||||
//! con distintos comportamientos según su finalidad, como enlaces de navegación o menús
|
||||
|
|
@ -6,26 +6,6 @@
|
|||
//!
|
||||
//! Los ítems pueden estar activos, deshabilitados o abrirse en nueva ventana según su contexto y
|
||||
//! configuración, y permiten incluir etiquetas localizables usando [`L10n`](pagetop::locale::L10n).
|
||||
//!
|
||||
//! # Ejemplo
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let nav = Nav::tabs()
|
||||
//! .with_layout(nav::Layout::End)
|
||||
//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
//! .with_item(nav::Item::link_blank(L10n::n("External"), |_| "https://docs.rs".into()))
|
||||
//! .with_item(nav::Item::dropdown(
|
||||
//! Dropdown::new()
|
||||
//! .with_title(L10n::n("Options"))
|
||||
//! .with_item(ChildOp::AddMany(vec![
|
||||
//! dropdown::Item::link(L10n::n("Action"), |_| "/action".into()).into(),
|
||||
//! dropdown::Item::link(L10n::n("Another"), |_| "/another".into()).into(),
|
||||
//! ])),
|
||||
//! ))
|
||||
//! .with_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into()));
|
||||
//! ```
|
||||
|
||||
mod props;
|
||||
pub use props::{Kind, Layout};
|
||||
|
|
|
|||
|
|
@ -1,15 +1,35 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::theme::*;
|
||||
|
||||
/// Componente para crear un **menú** o alguna de sus variantes ([`nav::Kind`]).
|
||||
/// Componente para crear un **menú** ([`nav`]).
|
||||
///
|
||||
/// Presenta un menú con una lista de elementos usando una vista básica, o alguna de sus variantes
|
||||
/// como *pestañas* (`Tabs`), *botones* (`Pills`) o *subrayado* (`Underline`). También permite
|
||||
/// controlar su distribución y orientación ([`nav::Layout`](crate::theme::nav::Layout)).
|
||||
/// ([`nav::Kind`]) como *pestañas* (`Tabs`), *botones* (`Pills`) o *subrayado* (`Underline`).
|
||||
/// También permite controlar su distribución y orientación ([`nav::Layout`](crate::theme::nav::Layout)).
|
||||
///
|
||||
/// Ver ejemplo en el módulo [`nav`].
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_bootsier::theme::*;
|
||||
///
|
||||
/// let nav = Nav::tabs()
|
||||
/// .with_layout(nav::Layout::End)
|
||||
/// .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
/// .with_item(nav::Item::link_blank(L10n::n("External"), |_| "https://docs.rs".into()))
|
||||
/// .with_item(nav::Item::dropdown(
|
||||
/// Dropdown::new()
|
||||
/// .with_title(L10n::n("Options"))
|
||||
/// .with_item(ChildOp::AddMany(vec![
|
||||
/// dropdown::Item::link(L10n::n("Action"), |_| "/action".into()).into(),
|
||||
/// dropdown::Item::link(L10n::n("Another"), |_| "/another".into()).into(),
|
||||
/// ])),
|
||||
/// ))
|
||||
/// .with_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into()));
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Nav {
|
||||
#[getters(skip)]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::theme::*;
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
// **< ItemKind >***********************************************************************************
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Definiciones para crear barras de navegación [`Navbar`].
|
||||
//! Definiciones para crear barras de navegación ([`Navbar`]).
|
||||
//!
|
||||
//! Cada [`navbar::Item`](crate::theme::navbar::Item) representa un elemento individual de la barra
|
||||
//! de navegación [`Navbar`], con distintos comportamientos según su finalidad, como menús
|
||||
|
|
@ -6,126 +6,6 @@
|
|||
//!
|
||||
//! También puede mostrar una marca de identidad ([`navbar::Brand`](crate::theme::navbar::Brand))
|
||||
//! que identifique la compañía, producto o nombre del proyecto asociado a la solución web.
|
||||
//!
|
||||
//! # Ejemplos
|
||||
//!
|
||||
//! Barra **simple**, sólo con un menú horizontal:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let navbar = Navbar::simple()
|
||||
//! .with_item(navbar::Item::nav(
|
||||
//! Nav::new()
|
||||
//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
//! .with_item(nav::Item::link(L10n::n("About"), |_| "/about".into()))
|
||||
//! .with_item(nav::Item::link(L10n::n("Contact"), |_| "/contact".into()))
|
||||
//! ));
|
||||
//! ```
|
||||
//!
|
||||
//! Barra **colapsable**, con botón de despliegue y contenido en el desplegable cuando colapsa:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let navbar = Navbar::simple_toggle()
|
||||
//! .with_expand(BreakPoint::MD)
|
||||
//! .with_item(navbar::Item::nav(
|
||||
//! Nav::new()
|
||||
//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
//! .with_item(nav::Item::link_blank(L10n::n("Docs"), |_| "https://docs.rs".into()))
|
||||
//! .with_item(nav::Item::link(L10n::n("Support"), |_| "/support".into()))
|
||||
//! ));
|
||||
//! ```
|
||||
//!
|
||||
//! Barra con **marca de identidad a la izquierda** y menú a la derecha, típica de una cabecera:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let brand = navbar::Brand::new()
|
||||
//! .with_title(L10n::n("PageTop"))
|
||||
//! .with_route(Some(|cx| cx.route("/")));
|
||||
//!
|
||||
//! let navbar = Navbar::brand_left(brand)
|
||||
//! .with_item(navbar::Item::nav(
|
||||
//! Nav::new()
|
||||
//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
//! .with_item(nav::Item::dropdown(
|
||||
//! Dropdown::new()
|
||||
//! .with_title(L10n::n("Tools"))
|
||||
//! .with_item(dropdown::Item::link(
|
||||
//! L10n::n("Generator"), |_| "/tools/gen".into())
|
||||
//! )
|
||||
//! .with_item(dropdown::Item::link(
|
||||
//! L10n::n("Reports"), |_| "/tools/reports".into())
|
||||
//! )
|
||||
//! ))
|
||||
//! .with_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into()))
|
||||
//! ));
|
||||
//! ```
|
||||
//!
|
||||
//! Barra con **botón de despliegue a la izquierda** y **marca de identidad a la derecha**:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let brand = navbar::Brand::new()
|
||||
//! .with_title(L10n::n("Intranet"))
|
||||
//! .with_route(Some(|cx| cx.route("/")));
|
||||
//!
|
||||
//! let navbar = Navbar::brand_right(brand)
|
||||
//! .with_expand(BreakPoint::LG)
|
||||
//! .with_item(navbar::Item::nav(
|
||||
//! Nav::pills()
|
||||
//! .with_item(nav::Item::link(L10n::n("Dashboard"), |_| "/dashboard".into()))
|
||||
//! .with_item(nav::Item::link(L10n::n("Users"), |_| "/users".into()))
|
||||
//! ));
|
||||
//! ```
|
||||
//!
|
||||
//! Barra con el **contenido en un *offcanvas***, ideal para dispositivos móviles o menús largos:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let oc = Offcanvas::new()
|
||||
//! .with_id("main_offcanvas")
|
||||
//! .with_title(L10n::n("Main menu"))
|
||||
//! .with_placement(offcanvas::Placement::Start)
|
||||
//! .with_backdrop(offcanvas::Backdrop::Enabled);
|
||||
//!
|
||||
//! let navbar = Navbar::offcanvas(oc)
|
||||
//! .with_item(navbar::Item::nav(
|
||||
//! Nav::new()
|
||||
//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
//! .with_item(nav::Item::link(L10n::n("Profile"), |_| "/profile".into()))
|
||||
//! .with_item(nav::Item::dropdown(
|
||||
//! Dropdown::new()
|
||||
//! .with_title(L10n::n("More"))
|
||||
//! .with_item(dropdown::Item::link(L10n::n("Settings"), |_| "/settings".into()))
|
||||
//! .with_item(dropdown::Item::link(L10n::n("Help"), |_| "/help".into()))
|
||||
//! ))
|
||||
//! ));
|
||||
//! ```
|
||||
//!
|
||||
//! Barra **fija arriba**:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let brand = navbar::Brand::new()
|
||||
//! .with_title(L10n::n("Main App"))
|
||||
//! .with_route(Some(|cx| cx.route("/")));
|
||||
//!
|
||||
//! let navbar = Navbar::brand_left(brand)
|
||||
//! .with_position(navbar::Position::FixedTop)
|
||||
//! .with_item(navbar::Item::nav(
|
||||
//! Nav::new()
|
||||
//! .with_item(nav::Item::link(L10n::n("Dashboard"), |_| "/".into()))
|
||||
//! .with_item(nav::Item::link(L10n::n("Donors"), |_| "/donors".into()))
|
||||
//! .with_item(nav::Item::link(L10n::n("Stock"), |_| "/stock".into()))
|
||||
//! ));
|
||||
//! ```
|
||||
|
||||
mod props;
|
||||
pub use props::{Layout, Position};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::theme::*;
|
||||
|
||||
/// Marca de identidad para mostrar en una barra de navegación [`Navbar`].
|
||||
///
|
||||
|
|
|
|||
|
|
@ -1,19 +1,139 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::theme::*;
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
const TOGGLE_COLLAPSE: &str = "collapse";
|
||||
const TOGGLE_OFFCANVAS: &str = "offcanvas";
|
||||
|
||||
/// Componente para crear una **barra de navegación**.
|
||||
/// Componente para crear una **barra de navegación** ([`navbar`]).
|
||||
///
|
||||
/// Permite mostrar enlaces, menús y una marca de identidad en distintas disposiciones (simples, con
|
||||
/// botón de despliegue o dentro de un [`offcanvas`]), controladas por [`navbar::Layout`]. También
|
||||
/// puede fijarse en la parte superior o inferior del documento mediante [`navbar::Position`].
|
||||
///
|
||||
/// Ver ejemplos en el módulo [`navbar`].
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// Barra **simple**, sólo con un menú horizontal:
|
||||
///
|
||||
/// ```rust
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_bootsier::theme::*;
|
||||
///
|
||||
/// let navbar = Navbar::simple()
|
||||
/// .with_item(navbar::Item::nav(
|
||||
/// Nav::new()
|
||||
/// .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
/// .with_item(nav::Item::link(L10n::n("About"), |_| "/about".into()))
|
||||
/// .with_item(nav::Item::link(L10n::n("Contact"), |_| "/contact".into()))
|
||||
/// ));
|
||||
/// ```
|
||||
///
|
||||
/// Barra **colapsable**, con botón de despliegue y contenido en el desplegable cuando colapsa:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let navbar = Navbar::simple_toggle()
|
||||
/// .with_expand(BreakPoint::MD)
|
||||
/// .with_item(navbar::Item::nav(
|
||||
/// Nav::new()
|
||||
/// .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
/// .with_item(nav::Item::link_blank(L10n::n("Docs"), |_| "https://docs.rs".into()))
|
||||
/// .with_item(nav::Item::link(L10n::n("Support"), |_| "/support".into()))
|
||||
/// ));
|
||||
/// ```
|
||||
///
|
||||
/// Barra con **marca de identidad a la izquierda** y menú a la derecha, típica de una cabecera:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let brand = navbar::Brand::new()
|
||||
/// .with_title(L10n::n("PageTop"))
|
||||
/// .with_route(Some(|cx| cx.route("/")));
|
||||
///
|
||||
/// let navbar = Navbar::brand_left(brand)
|
||||
/// .with_item(navbar::Item::nav(
|
||||
/// Nav::new()
|
||||
/// .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
/// .with_item(nav::Item::dropdown(
|
||||
/// Dropdown::new()
|
||||
/// .with_title(L10n::n("Tools"))
|
||||
/// .with_item(dropdown::Item::link(
|
||||
/// L10n::n("Generator"), |_| "/tools/gen".into())
|
||||
/// )
|
||||
/// .with_item(dropdown::Item::link(
|
||||
/// L10n::n("Reports"), |_| "/tools/reports".into())
|
||||
/// )
|
||||
/// ))
|
||||
/// .with_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into()))
|
||||
/// ));
|
||||
/// ```
|
||||
///
|
||||
/// Barra con **botón de despliegue a la izquierda** y **marca de identidad a la derecha**:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let brand = navbar::Brand::new()
|
||||
/// .with_title(L10n::n("Intranet"))
|
||||
/// .with_route(Some(|cx| cx.route("/")));
|
||||
///
|
||||
/// let navbar = Navbar::brand_right(brand)
|
||||
/// .with_expand(BreakPoint::LG)
|
||||
/// .with_item(navbar::Item::nav(
|
||||
/// Nav::pills()
|
||||
/// .with_item(nav::Item::link(L10n::n("Dashboard"), |_| "/dashboard".into()))
|
||||
/// .with_item(nav::Item::link(L10n::n("Users"), |_| "/users".into()))
|
||||
/// ));
|
||||
/// ```
|
||||
///
|
||||
/// Barra con el **contenido en un *offcanvas***, ideal para dispositivos móviles o menús largos:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let oc = Offcanvas::new()
|
||||
/// .with_id("main_offcanvas")
|
||||
/// .with_title(L10n::n("Main menu"))
|
||||
/// .with_placement(offcanvas::Placement::Start)
|
||||
/// .with_backdrop(offcanvas::Backdrop::Enabled);
|
||||
///
|
||||
/// let navbar = Navbar::offcanvas(oc)
|
||||
/// .with_item(navbar::Item::nav(
|
||||
/// Nav::new()
|
||||
/// .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
/// .with_item(nav::Item::link(L10n::n("Profile"), |_| "/profile".into()))
|
||||
/// .with_item(nav::Item::dropdown(
|
||||
/// Dropdown::new()
|
||||
/// .with_title(L10n::n("More"))
|
||||
/// .with_item(dropdown::Item::link(L10n::n("Settings"), |_| "/settings".into()))
|
||||
/// .with_item(dropdown::Item::link(L10n::n("Help"), |_| "/help".into()))
|
||||
/// ))
|
||||
/// ));
|
||||
/// ```
|
||||
///
|
||||
/// Barra **fija arriba**:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::theme::*;
|
||||
/// let brand = navbar::Brand::new()
|
||||
/// .with_title(L10n::n("Main App"))
|
||||
/// .with_route(Some(|cx| cx.route("/")));
|
||||
///
|
||||
/// let navbar = Navbar::brand_left(brand)
|
||||
/// .with_position(navbar::Position::FixedTop)
|
||||
/// .with_item(navbar::Item::nav(
|
||||
/// Nav::new()
|
||||
/// .with_item(nav::Item::link(L10n::n("Dashboard"), |_| "/".into()))
|
||||
/// .with_item(nav::Item::link(L10n::n("Donors"), |_| "/donors".into()))
|
||||
/// .with_item(nav::Item::link(L10n::n("Stock"), |_| "/stock".into()))
|
||||
/// ));
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Navbar {
|
||||
#[getters(skip)]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::theme::*;
|
||||
|
||||
/// Elementos que puede contener una barra de navegación [`Navbar`](crate::theme::Navbar).
|
||||
///
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::theme::*;
|
||||
|
||||
// **< Layout >*************************************************************************************
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,4 @@
|
|||
//! Definiciones para crear paneles laterales deslizantes [`Offcanvas`].
|
||||
//!
|
||||
//! # Ejemplo
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop::prelude::*;
|
||||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let panel = Offcanvas::new()
|
||||
//! .with_id("offcanvas_example")
|
||||
//! .with_title(L10n::n("Offcanvas title"))
|
||||
//! .with_placement(offcanvas::Placement::End)
|
||||
//! .with_backdrop(offcanvas::Backdrop::Enabled)
|
||||
//! .with_body_scroll(offcanvas::BodyScroll::Enabled)
|
||||
//! .with_visibility(offcanvas::Visibility::Default)
|
||||
//! .with_child(Dropdown::new()
|
||||
//! .with_title(L10n::n("Menu"))
|
||||
//! .with_item(dropdown::Item::label(L10n::n("Label")))
|
||||
//! .with_item(dropdown::Item::link_blank(L10n::n("Docs"), |_| "https://docs.rs".into()))
|
||||
//! .with_item(dropdown::Item::link(L10n::n("Sign out"), |_| "/signout".into()))
|
||||
//! );
|
||||
//! ```
|
||||
//! Definiciones para crear paneles laterales deslizantes ([`Offcanvas`]).
|
||||
|
||||
mod props;
|
||||
pub use props::{Backdrop, BodyScroll, Placement, Visibility};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::theme::*;
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
/// Componente para crear un **panel lateral deslizante** con contenidos adicionales.
|
||||
/// Componente para crear un **panel lateral deslizante** ([`offcanvas`]).
|
||||
///
|
||||
/// Útil para navegación, filtros, formularios o menús contextuales. Incluye las siguientes
|
||||
/// características principales:
|
||||
|
|
@ -19,8 +19,28 @@ use crate::LOCALES_BOOTSIER;
|
|||
/// - Asocia título y controles de accesibilidad a un identificador único y expone atributos
|
||||
/// adecuados para lectores de pantalla y navegación por teclado.
|
||||
///
|
||||
/// Ver ejemplo en el módulo [`offcanvas`].
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// use pagetop::prelude::*;
|
||||
/// use pagetop_bootsier::theme::*;
|
||||
///
|
||||
/// let panel = Offcanvas::new()
|
||||
/// .with_id("offcanvas_example")
|
||||
/// .with_title(L10n::n("Offcanvas title"))
|
||||
/// .with_placement(offcanvas::Placement::End)
|
||||
/// .with_backdrop(offcanvas::Backdrop::Enabled)
|
||||
/// .with_body_scroll(offcanvas::BodyScroll::Enabled)
|
||||
/// .with_visibility(offcanvas::Visibility::Default)
|
||||
/// .with_child(Dropdown::new()
|
||||
/// .with_title(L10n::n("Menu"))
|
||||
/// .with_item(dropdown::Item::label(L10n::n("Label")))
|
||||
/// .with_item(dropdown::Item::link_blank(L10n::n("Docs"), |_| "https://docs.rs".into()))
|
||||
/// .with_item(dropdown::Item::link(L10n::n("Sign out"), |_| "/signout".into()))
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Offcanvas {
|
||||
#[getters(skip)]
|
||||
|
|
|
|||
36
extensions/pagetop-seaorm/Cargo.toml
Normal file
36
extensions/pagetop-seaorm/Cargo.toml
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
[package]
|
||||
name = "pagetop-seaorm"
|
||||
version = "0.0.4"
|
||||
edition = "2021"
|
||||
|
||||
description = """
|
||||
Proporciona a PageTop acceso basado en SeaORM a bases de datos relacionales.
|
||||
"""
|
||||
categories = ["database", "development-tools", "asynchronous"]
|
||||
keywords = ["pagetop", "database", "sql", "orm", "ssr"]
|
||||
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[features]
|
||||
mysql = ["sea-orm/sqlx-mysql"]
|
||||
postgres = ["sea-orm/sqlx-postgres"]
|
||||
sqlite = ["sea-orm/sqlx-sqlite"]
|
||||
|
||||
[dependencies]
|
||||
pagetop.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
async-trait = "0.1"
|
||||
futures = "0.3"
|
||||
url = "2.5"
|
||||
|
||||
[dependencies.sea-orm]
|
||||
version = "1.1"
|
||||
features = ["debug-print", "macros", "runtime-async-std-native-tls"]
|
||||
default-features = false
|
||||
|
||||
[dependencies.sea-schema]
|
||||
version = "0.16"
|
||||
201
extensions/pagetop-seaorm/LICENSE-APACHE
Normal file
201
extensions/pagetop-seaorm/LICENSE-APACHE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2022 Manuel Cillero
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
21
extensions/pagetop-seaorm/LICENSE-MIT
Normal file
21
extensions/pagetop-seaorm/LICENSE-MIT
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Manuel Cillero
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
150
extensions/pagetop-seaorm/README.md
Normal file
150
extensions/pagetop-seaorm/README.md
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
<div align="center">
|
||||
|
||||
<h1>PageTop SeaORM</h1>
|
||||
|
||||
<p>Proporciona a <strong>PageTop</strong> acceso basado en <a href="https://www.sea-ql.org/SeaORM">SeaORM</a> a bases de datos relacionales.</p>
|
||||
|
||||
[](https://docs.rs/pagetop-seaorm)
|
||||
[](https://crates.io/crates/pagetop-seaorm)
|
||||
[](https://crates.io/crates/pagetop-seaorm)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-seaorm#licencia)
|
||||
|
||||
</div>
|
||||
|
||||
## 🧭 Sobre PageTop
|
||||
|
||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||
configurables, basadas en HTML, CSS y JavaScript.
|
||||
|
||||
|
||||
## ⚡️ Guía rápida
|
||||
|
||||
**Añade la dependencia** a tu `Cargo.toml` activando el motor de base de datos que necesites:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
pagetop-seaorm = { version = "...", features = ["sqlite"] }
|
||||
```
|
||||
|
||||
Las *features* disponibles son `mysql`, `postgres` y `sqlite`.
|
||||
|
||||
**Configura la conexión** en el archivo de configuración de la aplicación:
|
||||
|
||||
```toml
|
||||
[database]
|
||||
db_type = "sqlite"
|
||||
db_name = "my_app.db"
|
||||
max_pool_size = 5
|
||||
```
|
||||
|
||||
Para MySQL o PostgreSQL añade también `db_user`, `db_pass` y `db_host`. El campo `db_port` es
|
||||
opcional; si se omite se usa el puerto predeterminado del motor.
|
||||
|
||||
**Declara la extensión** en tu aplicación o en la extensión que la requiera:
|
||||
|
||||
```rust,ignore
|
||||
use pagetop::prelude::*;
|
||||
|
||||
struct MyApp;
|
||||
|
||||
impl Extension for MyApp {
|
||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||
vec![
|
||||
&pagetop_seaorm::SeaORM,
|
||||
]
|
||||
}
|
||||
|
||||
fn initialize(&self) {
|
||||
install_migrations!(m20240101_000001_create_users_table);
|
||||
}
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&MyApp).run()?.await
|
||||
}
|
||||
```
|
||||
|
||||
**Escribe las migraciones** usando la API de SeaORM:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop_seaorm::migration::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
table_auto(Users::Table)
|
||||
.col(pk_auto(Users::Id))
|
||||
.col(string_uniq(Users::Email))
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Users {
|
||||
Table,
|
||||
Id,
|
||||
Email,
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 📚 Créditos
|
||||
|
||||
Este *crate* se apoya en bibliotecas del ecosistema [SeaQL](https://github.com/SeaQL) como:
|
||||
|
||||
* [SeaORM](https://www.sea-ql.org/SeaORM), ORM asíncrono que usa internamente
|
||||
[SQLx](https://github.com/launchbadge/sqlx) para el acceso y la ejecución de consultas a la base
|
||||
de datos.
|
||||
|
||||
* [SeaQuery](https://github.com/SeaQL/sea-query), generador de consultas SQL sobre el que se
|
||||
construye el motor de migraciones y los *helpers* de esquema.
|
||||
|
||||
* [sea-schema](https://github.com/SeaQL/sea-schema), librería de introspección de esquemas SQL,
|
||||
usada por el módulo de migraciones para interrogar la estructura real de la base de datos (tablas,
|
||||
columnas, índices y claves externas).
|
||||
|
||||
El módulo de migraciones (`src/migration/`) incorpora una adaptación de
|
||||
[sea-orm-migration](https://crates.io/crates/sea-orm-migration). El código que se integra procede de
|
||||
la versión [**1.1.20**](https://github.com/SeaQL/sea-orm/tree/1.1.20/sea-orm-migration) en lugar de
|
||||
usarlo como dependencia ya que su paradigma de CLI no es compatible con el ciclo de vida de las
|
||||
extensiones de PageTop, donde las migraciones deben ejecutarse durante la inicialización de cada
|
||||
extensión. Los ficheros adaptados del original son:
|
||||
|
||||
| Archivos | Observaciones |
|
||||
|-----------------------|--------------------------------------------------------------------------|
|
||||
| `lib.rs` | Incluido en `migration.rs`, descarta módulos y exportaciones del CLI |
|
||||
| `connection.rs` | Integración completa |
|
||||
| `manager.rs` | Adapta *features* propias |
|
||||
| `migrator.rs` | Adapta *features* propias y omite gestión de errores del CLI |
|
||||
| `prelude.rs` | Absorbido en `migration.rs`, descarta exportaciones del CLI |
|
||||
| `schema.rs` | Integra con ajustes, original de [loco](https://github.com/loco-rs/loco) |
|
||||
| `seaql_migrations.rs` | Integración completa |
|
||||
|
||||
|
||||
## 🚧 Advertencia
|
||||
|
||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
||||
hasta que se libere la versión **1.0.0**.
|
||||
|
||||
|
||||
## 📜 Licencia
|
||||
|
||||
El código está disponible bajo una doble licencia:
|
||||
|
||||
* **Licencia MIT**
|
||||
([LICENSE-MIT](LICENSE-MIT) o también https://opensource.org/licenses/MIT)
|
||||
|
||||
* **Licencia Apache, Versión 2.0**
|
||||
([LICENSE-APACHE](LICENSE-APACHE) o también https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en
|
||||
el ecosistema Rust.
|
||||
64
extensions/pagetop-seaorm/src/config.rs
Normal file
64
extensions/pagetop-seaorm/src/config.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
//! Opciones de configuración de la extensión.
|
||||
//!
|
||||
//! Ejemplo:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [database]
|
||||
//! db_type = "mysql"
|
||||
//! db_name = "db"
|
||||
//! db_user = "user"
|
||||
//! db_pass = "password"
|
||||
//! db_host = "localhost"
|
||||
//! db_port = 3306
|
||||
//! max_pool_size = 5
|
||||
//! ```
|
||||
//!
|
||||
//! Uso:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use pagetop_seaorm::config;
|
||||
//! assert_eq!(config::SETTINGS.database.db_host, "localhost");
|
||||
//! ```
|
||||
//!
|
||||
//! Consulta [`pagetop::config`] para ver cómo PageTop lee los archivos de configuración y aplica
|
||||
//! los valores a los ajustes.
|
||||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
include_config!(SETTINGS: Settings => [
|
||||
// [database]
|
||||
"database.db_type" => "",
|
||||
"database.db_name" => "",
|
||||
"database.db_user" => "",
|
||||
"database.db_pass" => "",
|
||||
"database.db_host" => "localhost",
|
||||
"database.max_pool_size" => 5,
|
||||
]);
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
/// Tipos para la sección [`[database]`](Database) de [`SETTINGS`].
|
||||
pub struct Settings {
|
||||
pub database: Database,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
/// Sección `[database]` de la configuración. Forma parte de [`Settings`].
|
||||
pub struct Database {
|
||||
/// Tipo de base de datos: *"mysql"*, *"postgres"* ó *"sqlite"*.
|
||||
pub db_type: String,
|
||||
/// Nombre (para mysql/postgres) o referencia (para sqlite) de la base de datos.
|
||||
pub db_name: String,
|
||||
/// Usuario de conexión a la base de datos (para mysql/postgres).
|
||||
pub db_user: String,
|
||||
/// Contraseña para la conexión a la base de datos (para mysql/postgres).
|
||||
pub db_pass: String,
|
||||
/// Servidor de conexión a la base de datos (para mysql/postgres).
|
||||
pub db_host: String,
|
||||
/// Puerto de conexión a la base de datos (para mysql/postgres). Si es `None` se usa el puerto
|
||||
/// predeterminado para el motor: 3306 para MySQL y 5432 para PostgreSQL.
|
||||
pub db_port: Option<u16>,
|
||||
/// Número máximo de conexiones habilitadas.
|
||||
pub max_pool_size: u32,
|
||||
}
|
||||
140
extensions/pagetop-seaorm/src/db.rs
Normal file
140
extensions/pagetop-seaorm/src/db.rs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
//! API completa de SeaORM para operaciones con la base de datos.
|
||||
//!
|
||||
//! Re-exporta el *prelude* de SeaORM (entidades, traits, tipos de valor, macros de derivación…)
|
||||
//! y expone tres funciones de consulta propias. Con una sola importación tienes todo lo necesario
|
||||
//! para definir entidades y realizar operaciones CRUD:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use pagetop_seaorm::db::*;
|
||||
//! ```
|
||||
//!
|
||||
//! Para definir el esquema de la base de datos o escribir migraciones usa además
|
||||
//! [`crate::migration`].
|
||||
|
||||
pub use sea_orm::prelude::*;
|
||||
|
||||
use sea_orm::sea_query::{
|
||||
MysqlQueryBuilder, PostgresQueryBuilder, QueryStatementWriter, SqliteQueryBuilder,
|
||||
};
|
||||
use sea_orm::{DatabaseBackend, ExecResult, Statement};
|
||||
|
||||
/// Devuelve una referencia al pool de conexiones para usarla con el sistema de entidades.
|
||||
///
|
||||
/// Permite pasar la conexión a los métodos `all`, `one`, `exec`, etc. del sistema de entidades
|
||||
/// de SeaORM. El coste de esta llamada es prácticamente nulo: sólo devuelve una referencia a un
|
||||
/// valor inicializado una sola vez al arrancar la aplicación.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop_seaorm::db::*;
|
||||
///
|
||||
/// // Consultas tipadas con el sistema de entidades de SeaORM:
|
||||
/// // let users = User::find().all(connection()).await?;
|
||||
/// // let user = User::find_by_id(1).one(connection()).await?;
|
||||
/// // User::insert(model).exec(connection()).await?;
|
||||
/// let _conn = connection();
|
||||
/// ```
|
||||
pub fn connection() -> &'static DatabaseConnection {
|
||||
&super::DBCONN
|
||||
}
|
||||
|
||||
/// Ejecuta una consulta para devolver todas las filas resultantes.
|
||||
///
|
||||
/// Acepta cualquier tipo que implemente [`crate::migration::QueryStatementWriter`] (p. ej. [`crate::migration::SelectStatement`]) y
|
||||
/// serializa la sentencia al dialecto de la base de datos configurada antes de ejecutarla. Cada
|
||||
/// fila se devuelve como un [`QueryResult`] sin tipar; extrae los valores con
|
||||
/// [`QueryResult::try_get`].
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop_seaorm::db::*;
|
||||
/// use pagetop_seaorm::migration::*;
|
||||
///
|
||||
/// async fn example() -> Result<(), DbErr> {
|
||||
/// let mut stmt = Query::select()
|
||||
/// .column(Asterisk)
|
||||
/// .from(Alias::new("users"))
|
||||
/// .to_owned();
|
||||
/// let rows = fetch_all(&mut stmt).await?;
|
||||
/// for row in rows {
|
||||
/// let name: String = row.try_get("", "name")?;
|
||||
/// println!("{name}");
|
||||
/// }
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn fetch_all<Q: QueryStatementWriter>(stmt: &mut Q) -> Result<Vec<QueryResult>, DbErr> {
|
||||
let dbconn = &*super::DBCONN;
|
||||
let dbbackend = dbconn.get_database_backend();
|
||||
dbconn
|
||||
.query_all(Statement::from_string(
|
||||
dbbackend,
|
||||
match dbbackend {
|
||||
DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder),
|
||||
DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder),
|
||||
DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder),
|
||||
},
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Ejecuta una consulta y devuelve sólo la primera fila, si existe.
|
||||
///
|
||||
/// Funciona igual que [`fetch_all`] pero detiene la ejecución tras la primera fila y devuelve
|
||||
/// `None` si la consulta no produce resultados.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop_seaorm::db::*;
|
||||
/// use pagetop_seaorm::migration::*;
|
||||
///
|
||||
/// async fn example() -> Result<(), DbErr> {
|
||||
/// let mut stmt = Query::select()
|
||||
/// .column(Asterisk)
|
||||
/// .from(Alias::new("users"))
|
||||
/// .and_where(Expr::col(Alias::new("id")).eq(1))
|
||||
/// .to_owned();
|
||||
/// if let Some(row) = fetch_one(&mut stmt).await? {
|
||||
/// let name: String = row.try_get("", "name")?;
|
||||
/// println!("{name}");
|
||||
/// }
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn fetch_one<Q: QueryStatementWriter>(
|
||||
stmt: &mut Q,
|
||||
) -> Result<Option<QueryResult>, DbErr> {
|
||||
let dbconn = &*super::DBCONN;
|
||||
let dbbackend = dbconn.get_database_backend();
|
||||
dbconn
|
||||
.query_one(Statement::from_string(
|
||||
dbbackend,
|
||||
match dbbackend {
|
||||
DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder),
|
||||
DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder),
|
||||
DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder),
|
||||
},
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Ejecuta una sentencia SQL en crudo (INSERT, UPDATE, DELETE…) y devuelve el resultado de
|
||||
/// la operación.
|
||||
///
|
||||
/// A diferencia de [`fetch_all`] y [`fetch_one`], no construye la consulta, sino que la recibe
|
||||
/// como cadena ya formada. Útil para sentencias avanzadas o para migraciones puntuales. El
|
||||
/// [`ExecResult`] devuelto permite consultar las filas afectadas o el último ID insertado.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop_seaorm::db::*;
|
||||
///
|
||||
/// async fn example() -> Result<(), DbErr> {
|
||||
/// let result = execute("DELETE FROM sessions WHERE expired = 1").await?;
|
||||
/// println!("Filas eliminadas: {}", result.rows_affected());
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn execute(stmt: impl Into<String>) -> Result<ExecResult, DbErr> {
|
||||
let dbconn = &*super::DBCONN;
|
||||
let dbbackend = dbconn.get_database_backend();
|
||||
dbconn
|
||||
.execute(Statement::from_string(dbbackend, stmt.into()))
|
||||
.await
|
||||
}
|
||||
190
extensions/pagetop-seaorm/src/lib.rs
Normal file
190
extensions/pagetop-seaorm/src/lib.rs
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
/*!
|
||||
<div align="center">
|
||||
|
||||
<h1>PageTop SeaORM</h1>
|
||||
|
||||
<p>Proporciona a <strong>PageTop</strong> acceso basado en <a href="https://www.sea-ql.org/SeaORM">SeaORM</a> a bases de datos relacionales.</p>
|
||||
|
||||
[](https://docs.rs/pagetop-seaorm)
|
||||
[](https://crates.io/crates/pagetop-seaorm)
|
||||
[](https://crates.io/crates/pagetop-seaorm)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-seaorm#licencia)
|
||||
|
||||
</div>
|
||||
|
||||
## 🧭 Sobre PageTop
|
||||
|
||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||
configurables, basadas en HTML, CSS y JavaScript.
|
||||
|
||||
|
||||
## ⚡️ Guía rápida
|
||||
|
||||
**Añade la dependencia** a tu `Cargo.toml` activando el motor de base de datos que necesites:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
pagetop-seaorm = { version = "...", features = ["sqlite"] }
|
||||
```
|
||||
|
||||
Las *features* disponibles son `mysql`, `postgres` y `sqlite`.
|
||||
|
||||
**Configura la conexión** en el archivo de configuración de la aplicación:
|
||||
|
||||
```toml
|
||||
[database]
|
||||
db_type = "sqlite"
|
||||
db_name = "my_app.db"
|
||||
max_pool_size = 5
|
||||
```
|
||||
|
||||
Para MySQL o PostgreSQL añade también `db_user`, `db_pass` y `db_host`. El campo `db_port` es
|
||||
opcional; si se omite se usa el puerto predeterminado del motor.
|
||||
|
||||
**Declara la extensión** en tu aplicación o en la extensión que la requiera:
|
||||
|
||||
```rust,ignore
|
||||
use pagetop::prelude::*;
|
||||
|
||||
struct MyApp;
|
||||
|
||||
impl Extension for MyApp {
|
||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||
vec![
|
||||
&pagetop_seaorm::SeaORM,
|
||||
]
|
||||
}
|
||||
|
||||
fn initialize(&self) {
|
||||
install_migrations!(m20240101_000001_create_users_table);
|
||||
}
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&MyApp).run()?.await
|
||||
}
|
||||
```
|
||||
|
||||
**Escribe las migraciones** usando la API de SeaORM:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop_seaorm::migration::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
table_auto(Users::Table)
|
||||
.col(pk_auto(Users::Id))
|
||||
.col(string_uniq(Users::Email))
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Users {
|
||||
Table,
|
||||
Id,
|
||||
Email,
|
||||
}
|
||||
```
|
||||
*/
|
||||
|
||||
#![doc(
|
||||
html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico"
|
||||
)]
|
||||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
|
||||
use url::Url;
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
include_locales!(LOCALES_SEAORM);
|
||||
|
||||
pub mod config;
|
||||
|
||||
pub mod db;
|
||||
|
||||
pub mod migration;
|
||||
|
||||
pub(crate) use futures::executor::block_on as run_now;
|
||||
|
||||
pub(crate) static DBCONN: LazyLock<DatabaseConnection> = LazyLock::new(|| {
|
||||
trace::info!(
|
||||
"Connecting to database \"{}\" using a pool of {} connections",
|
||||
&config::SETTINGS.database.db_name,
|
||||
&config::SETTINGS.database.max_pool_size
|
||||
);
|
||||
|
||||
let db_uri = match config::SETTINGS.database.db_type.as_str() {
|
||||
"mysql" | "postgres" => {
|
||||
let mut tmp_uri = Url::parse(
|
||||
format!(
|
||||
"{}://{}/{}",
|
||||
&config::SETTINGS.database.db_type,
|
||||
&config::SETTINGS.database.db_host,
|
||||
&config::SETTINGS.database.db_name
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.unwrap();
|
||||
tmp_uri
|
||||
.set_username(config::SETTINGS.database.db_user.as_str())
|
||||
.unwrap();
|
||||
// https://github.com/launchbadge/sqlx/issues/1624
|
||||
tmp_uri
|
||||
.set_password(Some(config::SETTINGS.database.db_pass.as_str()))
|
||||
.unwrap();
|
||||
if let Some(port) = config::SETTINGS.database.db_port {
|
||||
tmp_uri.set_port(Some(port)).unwrap();
|
||||
}
|
||||
tmp_uri
|
||||
}
|
||||
"sqlite" => Url::parse(
|
||||
format!(
|
||||
"{}://{}",
|
||||
&config::SETTINGS.database.db_type,
|
||||
&config::SETTINGS.database.db_name
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.unwrap(),
|
||||
_ => panic!(
|
||||
"Unrecognized database type \"{}\"",
|
||||
config::SETTINGS.database.db_type
|
||||
),
|
||||
};
|
||||
|
||||
run_now(Database::connect::<ConnectOptions>({
|
||||
let mut db_opt = ConnectOptions::new(db_uri.to_string());
|
||||
db_opt.max_connections(config::SETTINGS.database.max_pool_size);
|
||||
db_opt
|
||||
}))
|
||||
.unwrap_or_else(|_| panic!("Failed to connect to database"))
|
||||
});
|
||||
|
||||
/// Implementa la extensión.
|
||||
pub struct SeaORM;
|
||||
|
||||
impl Extension for SeaORM {
|
||||
fn name(&self) -> L10n {
|
||||
L10n::t("extension_name", &LOCALES_SEAORM)
|
||||
}
|
||||
|
||||
fn description(&self) -> L10n {
|
||||
L10n::t("extension_description", &LOCALES_SEAORM)
|
||||
}
|
||||
|
||||
fn initialize(&self) {
|
||||
std::sync::LazyLock::force(&DBCONN);
|
||||
}
|
||||
}
|
||||
2
extensions/pagetop-seaorm/src/locale/en-US/extension.ftl
Normal file
2
extensions/pagetop-seaorm/src/locale/en-US/extension.ftl
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
extension_name = SeaORM support
|
||||
extension_description = Provides SeaORM-based access to relational databases.
|
||||
2
extensions/pagetop-seaorm/src/locale/es-ES/extension.ftl
Normal file
2
extensions/pagetop-seaorm/src/locale/es-ES/extension.ftl
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
extension_name = Soporte a SeaORM
|
||||
extension_description = Proporciona acceso basado en SeaORM a bases de datos relacionales.
|
||||
156
extensions/pagetop-seaorm/src/migration.rs
Normal file
156
extensions/pagetop-seaorm/src/migration.rs
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
//! API para definir y ejecutar migraciones de base de datos.
|
||||
//!
|
||||
//! Re-exporta los tipos de SeaORM necesarios para escribir migraciones y ofrece las macros
|
||||
//! [`crate::install_migrations`] y [`crate::uninstall_migrations`] para aplicarlas o revertirlas al
|
||||
//! arrancar la extensión.
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use pagetop_seaorm::db::*;
|
||||
//! use pagetop_seaorm::migration::*;
|
||||
//! ```
|
||||
|
||||
// **< Adaptación de `sea-orm-migration` (ver §Créditos en README.md) >*****************************
|
||||
|
||||
//pub mod cli;
|
||||
pub mod connection;
|
||||
pub mod manager;
|
||||
pub mod migrator;
|
||||
//pub mod prelude;
|
||||
pub mod schema;
|
||||
pub mod seaql_migrations;
|
||||
//pub mod util;
|
||||
|
||||
pub use connection::*;
|
||||
pub use manager::*;
|
||||
//pub use migrator::*;
|
||||
|
||||
pub use async_trait;
|
||||
//pub use sea_orm;
|
||||
//pub use sea_orm::sea_query;
|
||||
pub 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(
|
||||
"Rollback not implemented for this migration".to_owned(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// *************************************************************************************************
|
||||
|
||||
pub use migrator::MigratorTrait;
|
||||
pub use schema::*;
|
||||
pub use sea_orm::sea_query::*;
|
||||
pub use sea_orm::DeriveIden;
|
||||
|
||||
use pagetop::core::TypeInfo;
|
||||
use pagetop::trace;
|
||||
|
||||
impl<M: MigrationTrait> MigrationName for M {
|
||||
fn name(&self) -> &str {
|
||||
TypeInfo::NameTo(-2).of::<M>()
|
||||
}
|
||||
}
|
||||
|
||||
pub type MigrationItem = Box<dyn MigrationTrait>;
|
||||
|
||||
pub trait MigratorBase {
|
||||
fn run_up();
|
||||
|
||||
fn run_down();
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
impl<M: MigratorTrait> MigratorBase for M {
|
||||
fn run_up() {
|
||||
if let Err(e) = super::run_now(Self::up(SchemaManagerConnection::Connection(&super::DBCONN), None)) {
|
||||
trace::error!("Migration upgrade failed ({})", e);
|
||||
};
|
||||
}
|
||||
|
||||
fn run_down() {
|
||||
if let Err(e) = super::run_now(Self::down(SchemaManagerConnection::Connection(&super::DBCONN), None)) {
|
||||
trace::error!("Migration downgrade failed ({})", e);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Aplica las migraciones pendientes al arrancar una extensión.
|
||||
///
|
||||
/// Recibe uno o más módulos de migración y ejecuta el método `up` de los que aún no estén
|
||||
/// registrados en la tabla `seaql_migrations`. Se invoca habitualmente desde
|
||||
/// [`Extension::initialize`](pagetop::core::extension::Extension::initialize).
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// impl Extension for MyExt {
|
||||
/// fn initialize(&self) {
|
||||
/// install_migrations!(
|
||||
/// m20240101_000001_create_users_table,
|
||||
/// m20240115_000002_add_email_index,
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! install_migrations {
|
||||
( $($migration_module:ident),+ $(,)? ) => {{
|
||||
use $crate::migration::{MigrationItem, MigratorBase, MigratorTrait};
|
||||
|
||||
struct Migrator;
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<MigrationItem> {
|
||||
let mut m = Vec::<MigrationItem>::new();
|
||||
$(
|
||||
m.push(Box::new(migration::$migration_module::Migration));
|
||||
)*
|
||||
m
|
||||
}
|
||||
}
|
||||
Migrator::run_up();
|
||||
}};
|
||||
}
|
||||
|
||||
/// Revierte las migraciones de una extensión en orden inverso al de su aplicación.
|
||||
///
|
||||
/// Ejecuta el método `down` de cada migración indicada. Si alguna no implementa `down`,
|
||||
/// detiene el proceso con un error. Complementario a [`crate::install_migrations`].
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// impl Extension for MyExt {
|
||||
/// fn uninitialize(&self) {
|
||||
/// uninstall_migrations!(
|
||||
/// m20240101_000001_create_users_table,
|
||||
/// m20240115_000002_add_email_index,
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! uninstall_migrations {
|
||||
( $($migration_module:ident),+ $(,)? ) => {{
|
||||
use $crate::migration::{MigrationItem, MigratorBase, MigratorTrait};
|
||||
|
||||
struct Migrator;
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<MigrationItem> {
|
||||
let mut m = Vec::<MigrationItem>::new();
|
||||
$(
|
||||
m.push(Box::new(migration::$migration_module::Migration));
|
||||
)*
|
||||
m
|
||||
}
|
||||
}
|
||||
Migrator::run_down();
|
||||
}};
|
||||
}
|
||||
148
extensions/pagetop-seaorm/src/migration/connection.rs
Normal file
148
extensions/pagetop-seaorm/src/migration/connection.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
use futures::Future;
|
||||
use sea_orm::{
|
||||
AccessMode, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbBackend, DbErr,
|
||||
ExecResult, IsolationLevel, QueryResult, Statement, TransactionError, TransactionTrait,
|
||||
};
|
||||
use std::pin::Pin;
|
||||
|
||||
pub enum SchemaManagerConnection<'c> {
|
||||
Connection(&'c DatabaseConnection),
|
||||
Transaction(&'c DatabaseTransaction),
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ConnectionTrait for SchemaManagerConnection<'_> {
|
||||
fn get_database_backend(&self) -> DbBackend {
|
||||
match self {
|
||||
SchemaManagerConnection::Connection(conn) => conn.get_database_backend(),
|
||||
SchemaManagerConnection::Transaction(trans) => trans.get_database_backend(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute(&self, stmt: Statement) -> Result<ExecResult, DbErr> {
|
||||
match self {
|
||||
SchemaManagerConnection::Connection(conn) => conn.execute(stmt).await,
|
||||
SchemaManagerConnection::Transaction(trans) => trans.execute(stmt).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_unprepared(&self, sql: &str) -> Result<ExecResult, DbErr> {
|
||||
match self {
|
||||
SchemaManagerConnection::Connection(conn) => conn.execute_unprepared(sql).await,
|
||||
SchemaManagerConnection::Transaction(trans) => trans.execute_unprepared(sql).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn query_one(&self, stmt: Statement) -> Result<Option<QueryResult>, DbErr> {
|
||||
match self {
|
||||
SchemaManagerConnection::Connection(conn) => conn.query_one(stmt).await,
|
||||
SchemaManagerConnection::Transaction(trans) => trans.query_one(stmt).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn query_all(&self, stmt: Statement) -> Result<Vec<QueryResult>, DbErr> {
|
||||
match self {
|
||||
SchemaManagerConnection::Connection(conn) => conn.query_all(stmt).await,
|
||||
SchemaManagerConnection::Transaction(trans) => trans.query_all(stmt).await,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_mock_connection(&self) -> bool {
|
||||
match self {
|
||||
SchemaManagerConnection::Connection(conn) => conn.is_mock_connection(),
|
||||
SchemaManagerConnection::Transaction(trans) => trans.is_mock_connection(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl TransactionTrait for SchemaManagerConnection<'_> {
|
||||
async fn begin(&self) -> Result<DatabaseTransaction, DbErr> {
|
||||
match self {
|
||||
SchemaManagerConnection::Connection(conn) => conn.begin().await,
|
||||
SchemaManagerConnection::Transaction(trans) => trans.begin().await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn begin_with_config(
|
||||
&self,
|
||||
isolation_level: Option<IsolationLevel>,
|
||||
access_mode: Option<AccessMode>,
|
||||
) -> Result<DatabaseTransaction, DbErr> {
|
||||
match self {
|
||||
SchemaManagerConnection::Connection(conn) => {
|
||||
conn.begin_with_config(isolation_level, access_mode).await
|
||||
}
|
||||
SchemaManagerConnection::Transaction(trans) => {
|
||||
trans.begin_with_config(isolation_level, access_mode).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn transaction<F, T, E>(&self, callback: F) -> Result<T, TransactionError<E>>
|
||||
where
|
||||
F: for<'a> FnOnce(
|
||||
&'a DatabaseTransaction,
|
||||
) -> Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'a>>
|
||||
+ Send,
|
||||
T: Send,
|
||||
E: std::fmt::Display + std::fmt::Debug + Send,
|
||||
{
|
||||
match self {
|
||||
SchemaManagerConnection::Connection(conn) => conn.transaction(callback).await,
|
||||
SchemaManagerConnection::Transaction(trans) => trans.transaction(callback).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn transaction_with_config<F, T, E>(
|
||||
&self,
|
||||
callback: F,
|
||||
isolation_level: Option<IsolationLevel>,
|
||||
access_mode: Option<AccessMode>,
|
||||
) -> Result<T, TransactionError<E>>
|
||||
where
|
||||
F: for<'a> FnOnce(
|
||||
&'a DatabaseTransaction,
|
||||
) -> Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'a>>
|
||||
+ Send,
|
||||
T: Send,
|
||||
E: std::fmt::Display + std::fmt::Debug + Send,
|
||||
{
|
||||
match self {
|
||||
SchemaManagerConnection::Connection(conn) => {
|
||||
conn.transaction_with_config(callback, isolation_level, access_mode)
|
||||
.await
|
||||
}
|
||||
SchemaManagerConnection::Transaction(trans) => {
|
||||
trans
|
||||
.transaction_with_config(callback, isolation_level, access_mode)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoSchemaManagerConnection<'c>: Send
|
||||
where
|
||||
Self: 'c,
|
||||
{
|
||||
fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c>;
|
||||
}
|
||||
|
||||
impl<'c> IntoSchemaManagerConnection<'c> for SchemaManagerConnection<'c> {
|
||||
fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'c> IntoSchemaManagerConnection<'c> for &'c DatabaseConnection {
|
||||
fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c> {
|
||||
SchemaManagerConnection::Connection(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'c> IntoSchemaManagerConnection<'c> for &'c DatabaseTransaction {
|
||||
fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c> {
|
||||
SchemaManagerConnection::Transaction(self)
|
||||
}
|
||||
}
|
||||
186
extensions/pagetop-seaorm/src/migration/manager.rs
Normal file
186
extensions/pagetop-seaorm/src/migration/manager.rs
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
use super::{IntoSchemaManagerConnection, SchemaManagerConnection};
|
||||
use sea_orm::sea_query::{
|
||||
extension::postgres::{TypeAlterStatement, TypeCreateStatement, TypeDropStatement},
|
||||
ForeignKeyCreateStatement, ForeignKeyDropStatement, IndexCreateStatement, IndexDropStatement,
|
||||
SelectStatement, TableAlterStatement, TableCreateStatement, TableDropStatement,
|
||||
TableRenameStatement, TableTruncateStatement,
|
||||
};
|
||||
use sea_orm::{ConnectionTrait, DbBackend, DbErr, StatementBuilder};
|
||||
#[allow(unused_imports)]
|
||||
use sea_schema::probe::SchemaProbe;
|
||||
|
||||
/// Helper struct for writing migration scripts in migration file
|
||||
pub struct SchemaManager<'c> {
|
||||
conn: SchemaManagerConnection<'c>,
|
||||
}
|
||||
|
||||
impl<'c> SchemaManager<'c> {
|
||||
pub fn new<T>(conn: T) -> Self
|
||||
where
|
||||
T: IntoSchemaManagerConnection<'c>,
|
||||
{
|
||||
Self {
|
||||
conn: conn.into_schema_manager_connection(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn exec_stmt<S>(&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) -> &SchemaManagerConnection<'c> {
|
||||
&self.conn
|
||||
}
|
||||
}
|
||||
|
||||
/// Schema Creation
|
||||
impl SchemaManager<'_> {
|
||||
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 SchemaManager<'_> {
|
||||
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 SchemaManager<'_> {
|
||||
pub async fn has_table<T>(&self, table: T) -> Result<bool, DbErr>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
has_table(&self.conn, table).await
|
||||
}
|
||||
|
||||
pub async fn has_column<T, C>(&self, _table: T, _column: C) -> Result<bool, DbErr>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
C: AsRef<str>,
|
||||
{
|
||||
let _stmt: SelectStatement = match self.conn.get_database_backend() {
|
||||
#[cfg(feature = "mysql")]
|
||||
DbBackend::MySql => sea_schema::mysql::MySql.has_column(_table, _column),
|
||||
#[cfg(feature = "postgres")]
|
||||
DbBackend::Postgres => sea_schema::postgres::Postgres.has_column(_table, _column),
|
||||
#[cfg(feature = "sqlite")]
|
||||
DbBackend::Sqlite => sea_schema::sqlite::Sqlite.has_column(_table, _column),
|
||||
#[allow(unreachable_patterns)]
|
||||
other => panic!("{other:?} feature is off"),
|
||||
};
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
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")
|
||||
}
|
||||
|
||||
pub async fn has_index<T, I>(&self, _table: T, _index: I) -> Result<bool, DbErr>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
I: AsRef<str>,
|
||||
{
|
||||
let _stmt: SelectStatement = match self.conn.get_database_backend() {
|
||||
#[cfg(feature = "mysql")]
|
||||
DbBackend::MySql => sea_schema::mysql::MySql.has_index(_table, _index),
|
||||
#[cfg(feature = "postgres")]
|
||||
DbBackend::Postgres => sea_schema::postgres::Postgres.has_index(_table, _index),
|
||||
#[cfg(feature = "sqlite")]
|
||||
DbBackend::Sqlite => sea_schema::sqlite::Sqlite.has_index(_table, _index),
|
||||
#[allow(unreachable_patterns)]
|
||||
other => panic!("{other:?} feature is off"),
|
||||
};
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
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 index exists".to_owned()))?;
|
||||
|
||||
res.try_get("", "has_index")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn has_table<C, T>(conn: &C, _table: T) -> Result<bool, DbErr>
|
||||
where
|
||||
C: ConnectionTrait,
|
||||
T: AsRef<str>,
|
||||
{
|
||||
let _stmt: SelectStatement = match conn.get_database_backend() {
|
||||
#[cfg(feature = "mysql")]
|
||||
DbBackend::MySql => sea_schema::mysql::MySql.has_table(_table),
|
||||
#[cfg(feature = "postgres")]
|
||||
DbBackend::Postgres => sea_schema::postgres::Postgres.has_table(_table),
|
||||
#[cfg(feature = "sqlite")]
|
||||
DbBackend::Sqlite => sea_schema::sqlite::Sqlite.has_table(_table),
|
||||
#[allow(unreachable_patterns)]
|
||||
other => panic!("{other:?} feature is off"),
|
||||
};
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
let builder = conn.get_database_backend();
|
||||
let res = conn
|
||||
.query_one(builder.build(&_stmt))
|
||||
.await?
|
||||
.ok_or_else(|| DbErr::Custom("Failed to check table exists".to_owned()))?;
|
||||
|
||||
res.try_get("", "has_table")
|
||||
}
|
||||
617
extensions/pagetop-seaorm/src/migration/migrator.rs
Normal file
617
extensions/pagetop-seaorm/src/migration/migrator.rs
Normal file
|
|
@ -0,0 +1,617 @@
|
|||
use futures::Future;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Display;
|
||||
use std::pin::Pin;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use pagetop::trace::info;
|
||||
|
||||
use sea_orm::sea_query::{
|
||||
self, extension::postgres::Type, Alias, Expr, ExprTrait, ForeignKey, IntoIden, Order, Query,
|
||||
SelectStatement, SimpleExpr, Table,
|
||||
};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ActiveValue, Condition, ConnectionTrait, DbBackend, DbErr, DeriveIden,
|
||||
DynIden, EntityTrait, FromQueryResult, Iterable, QueryFilter, Schema, Statement,
|
||||
TransactionTrait,
|
||||
};
|
||||
#[allow(unused_imports)]
|
||||
use sea_schema::probe::SchemaProbe;
|
||||
|
||||
use super::{seaql_migrations, IntoSchemaManagerConnection, MigrationTrait, SchemaManager};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
/// Status of migration
|
||||
pub enum MigrationStatus {
|
||||
/// Not yet applied
|
||||
Pending,
|
||||
/// Applied
|
||||
Applied,
|
||||
}
|
||||
|
||||
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}")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Migration {
|
||||
migration: Box<dyn MigrationTrait>,
|
||||
status: MigrationStatus,
|
||||
}
|
||||
|
||||
impl Migration {
|
||||
/// Get migration name from MigrationName trait implementation
|
||||
pub fn name(&self) -> &str {
|
||||
self.migration.name()
|
||||
}
|
||||
|
||||
/// Get migration status
|
||||
pub fn status(&self) -> MigrationStatus {
|
||||
self.status
|
||||
}
|
||||
}
|
||||
|
||||
/// Performing migrations on a database
|
||||
#[async_trait::async_trait]
|
||||
pub trait MigratorTrait: Send {
|
||||
/// Vector of migrations in time sequence
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>>;
|
||||
|
||||
/// Name of the migration table, it is `seaql_migrations` by default
|
||||
fn migration_table_name() -> DynIden {
|
||||
seaql_migrations::Entity.into_iden()
|
||||
}
|
||||
|
||||
/// Get list of migrations wrapped in `Migration` struct
|
||||
fn get_migration_files() -> Vec<Migration> {
|
||||
Self::migrations()
|
||||
.into_iter()
|
||||
.map(|migration| Migration {
|
||||
migration,
|
||||
status: MigrationStatus::Pending,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get list of applied migrations from database
|
||||
async fn get_migration_models<C>(db: &C) -> Result<Vec<seaql_migrations::Model>, DbErr>
|
||||
where
|
||||
C: ConnectionTrait,
|
||||
{
|
||||
Self::install(db).await?;
|
||||
let stmt = Query::select()
|
||||
.table_name(Self::migration_table_name())
|
||||
.columns(seaql_migrations::Column::iter().map(IntoIden::into_iden))
|
||||
.order_by(seaql_migrations::Column::Version, Order::Asc)
|
||||
.to_owned();
|
||||
let builder = db.get_database_backend();
|
||||
seaql_migrations::Model::find_by_statement(builder.build(&stmt))
|
||||
.all(db)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get list of migrations with status
|
||||
async fn get_migration_with_status<C>(db: &C) -> Result<Vec<Migration>, DbErr>
|
||||
where
|
||||
C: ConnectionTrait,
|
||||
{
|
||||
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<String> = migration_models
|
||||
.into_iter()
|
||||
.map(|model| model.version)
|
||||
.collect();
|
||||
let migration_in_fs: HashSet<String> = 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<String> = missing_migrations_in_fs
|
||||
.iter()
|
||||
.map(|missing_migration| {
|
||||
format!("Migration file of version '{missing_migration}' is missing, this migration has been applied but its file is missing")
|
||||
}).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<C>(db: &C) -> Result<Vec<Migration>, DbErr>
|
||||
where
|
||||
C: ConnectionTrait,
|
||||
{
|
||||
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<C>(db: &C) -> Result<Vec<Migration>, DbErr>
|
||||
where
|
||||
C: ConnectionTrait,
|
||||
{
|
||||
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<C>(db: &C) -> Result<(), DbErr>
|
||||
where
|
||||
C: ConnectionTrait,
|
||||
{
|
||||
let builder = db.get_database_backend();
|
||||
let table_name = Self::migration_table_name();
|
||||
let schema = Schema::new(builder);
|
||||
let mut stmt = schema
|
||||
.create_table_from_entity(seaql_migrations::Entity)
|
||||
.table_name(table_name);
|
||||
stmt.if_not_exists();
|
||||
db.execute(builder.build(&stmt)).await.map(|_| ())
|
||||
}
|
||||
|
||||
/// Check the status of all migrations
|
||||
async fn status<C>(db: &C) -> Result<(), DbErr>
|
||||
where
|
||||
C: ConnectionTrait,
|
||||
{
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Drop all tables from the database, then reapply all migrations
|
||||
async fn fresh<'c, C>(db: C) -> Result<(), DbErr>
|
||||
where
|
||||
C: IntoSchemaManagerConnection<'c>,
|
||||
{
|
||||
exec_with_connection::<'_, _, _>(db, move |manager| {
|
||||
Box::pin(async move { exec_fresh::<Self>(manager).await })
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Rollback all applied migrations, then reapply all migrations
|
||||
async fn refresh<'c, C>(db: C) -> Result<(), DbErr>
|
||||
where
|
||||
C: IntoSchemaManagerConnection<'c>,
|
||||
{
|
||||
exec_with_connection::<'_, _, _>(db, move |manager| {
|
||||
Box::pin(async move {
|
||||
exec_down::<Self>(manager, None).await?;
|
||||
exec_up::<Self>(manager, None).await
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Rollback all applied migrations
|
||||
async fn reset<'c, C>(db: C) -> Result<(), DbErr>
|
||||
where
|
||||
C: IntoSchemaManagerConnection<'c>,
|
||||
{
|
||||
exec_with_connection::<'_, _, _>(db, move |manager| {
|
||||
Box::pin(async move { exec_down::<Self>(manager, None).await })
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Apply pending migrations
|
||||
async fn up<'c, C>(db: C, steps: Option<u32>) -> Result<(), DbErr>
|
||||
where
|
||||
C: IntoSchemaManagerConnection<'c>,
|
||||
{
|
||||
exec_with_connection::<'_, _, _>(db, move |manager| {
|
||||
Box::pin(async move { exec_up::<Self>(manager, steps).await })
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Rollback applied migrations
|
||||
async fn down<'c, C>(db: C, steps: Option<u32>) -> Result<(), DbErr>
|
||||
where
|
||||
C: IntoSchemaManagerConnection<'c>,
|
||||
{
|
||||
exec_with_connection::<'_, _, _>(db, move |manager| {
|
||||
Box::pin(async move { exec_down::<Self>(manager, steps).await })
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
async fn exec_with_connection<'c, C, F>(db: C, f: F) -> Result<(), DbErr>
|
||||
where
|
||||
C: IntoSchemaManagerConnection<'c>,
|
||||
F: for<'b> Fn(
|
||||
&'b SchemaManager<'_>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<(), DbErr>> + Send + 'b>>,
|
||||
{
|
||||
let db = db.into_schema_manager_connection();
|
||||
|
||||
match db.get_database_backend() {
|
||||
DbBackend::Postgres => {
|
||||
let transaction = db.begin().await?;
|
||||
let manager = SchemaManager::new(&transaction);
|
||||
f(&manager).await?;
|
||||
transaction.commit().await
|
||||
}
|
||||
DbBackend::MySql | DbBackend::Sqlite => {
|
||||
let manager = SchemaManager::new(db);
|
||||
f(&manager).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn exec_fresh<M>(manager: &SchemaManager<'_>) -> Result<(), DbErr>
|
||||
where
|
||||
M: MigratorTrait + ?Sized,
|
||||
{
|
||||
let db = manager.get_connection();
|
||||
|
||||
M::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 stmt = query_mysql_foreign_keys(db);
|
||||
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).await;
|
||||
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);
|
||||
}
|
||||
|
||||
// Drop all types
|
||||
if db_backend == DbBackend::Postgres {
|
||||
info!("Dropping all types");
|
||||
let stmt = query_pg_types(db);
|
||||
let rows = db.query_all(db_backend.build(&stmt)).await?;
|
||||
for row in rows {
|
||||
let type_name: String = row.try_get("", "typname")?;
|
||||
info!("Dropping type '{}'", type_name);
|
||||
let mut stmt = Type::drop();
|
||||
stmt.name(Alias::new(&type_name));
|
||||
db.execute(db_backend.build(&stmt)).await?;
|
||||
info!("Type '{}' has been dropped", type_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
|
||||
exec_up::<M>(manager, None).await
|
||||
}
|
||||
|
||||
async fn exec_up<M>(manager: &SchemaManager<'_>, mut steps: Option<u32>) -> Result<(), DbErr>
|
||||
where
|
||||
M: MigratorTrait + ?Sized,
|
||||
{
|
||||
let db = manager.get_connection();
|
||||
|
||||
M::install(db).await?;
|
||||
/*
|
||||
if let Some(steps) = steps {
|
||||
info!("Applying {} pending migrations", steps);
|
||||
} else {
|
||||
info!("Applying all pending migrations");
|
||||
}
|
||||
*/
|
||||
let migrations = M::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::Entity::insert(seaql_migrations::ActiveModel {
|
||||
version: ActiveValue::Set(migration.name().to_owned()),
|
||||
applied_at: ActiveValue::Set(now.as_secs() as i64),
|
||||
})
|
||||
.table_name(M::migration_table_name())
|
||||
.exec(db)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn exec_down<M>(manager: &SchemaManager<'_>, mut steps: Option<u32>) -> Result<(), DbErr>
|
||||
where
|
||||
M: MigratorTrait + ?Sized,
|
||||
{
|
||||
let db = manager.get_connection();
|
||||
|
||||
M::install(db).await?;
|
||||
|
||||
if let Some(steps) = steps {
|
||||
info!("Rolling back {} applied migrations", steps);
|
||||
} else {
|
||||
info!("Rolling back all applied migrations");
|
||||
}
|
||||
|
||||
let migrations = M::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(Expr::col(seaql_migrations::Column::Version).eq(migration.name()))
|
||||
.table_name(M::migration_table_name())
|
||||
.exec(db)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn query_tables<C>(db: &C) -> SelectStatement
|
||||
where
|
||||
C: ConnectionTrait,
|
||||
{
|
||||
match db.get_database_backend() {
|
||||
#[cfg(feature = "mysql")]
|
||||
DbBackend::MySql => sea_schema::mysql::MySql.query_tables(),
|
||||
#[cfg(feature = "postgres")]
|
||||
DbBackend::Postgres => sea_schema::postgres::Postgres.query_tables(),
|
||||
#[cfg(feature = "sqlite")]
|
||||
DbBackend::Sqlite => sea_schema::sqlite::Sqlite.query_tables(),
|
||||
#[allow(unreachable_patterns)]
|
||||
other => panic!("{other:?} feature is off"),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_schema<C>(db: &C) -> SimpleExpr
|
||||
where
|
||||
C: ConnectionTrait,
|
||||
{
|
||||
match db.get_database_backend() {
|
||||
#[cfg(feature = "mysql")]
|
||||
DbBackend::MySql => sea_schema::mysql::MySql::get_current_schema(),
|
||||
#[cfg(feature = "postgres")]
|
||||
DbBackend::Postgres => sea_schema::postgres::Postgres::get_current_schema(),
|
||||
#[cfg(feature = "sqlite")]
|
||||
DbBackend::Sqlite => sea_schema::sqlite::Sqlite::get_current_schema(),
|
||||
#[allow(unreachable_patterns)]
|
||||
other => panic!("{other:?} feature is off"),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum InformationSchema {
|
||||
#[sea_orm(iden = "information_schema")]
|
||||
Schema,
|
||||
#[sea_orm(iden = "TABLE_NAME")]
|
||||
TableName,
|
||||
#[sea_orm(iden = "CONSTRAINT_NAME")]
|
||||
ConstraintName,
|
||||
TableConstraints,
|
||||
TableSchema,
|
||||
ConstraintType,
|
||||
}
|
||||
|
||||
fn query_mysql_foreign_keys<C>(db: &C) -> SelectStatement
|
||||
where
|
||||
C: ConnectionTrait,
|
||||
{
|
||||
let mut stmt = Query::select();
|
||||
stmt.columns([
|
||||
InformationSchema::TableName,
|
||||
InformationSchema::ConstraintName,
|
||||
])
|
||||
.from((
|
||||
InformationSchema::Schema,
|
||||
InformationSchema::TableConstraints,
|
||||
))
|
||||
.cond_where(
|
||||
Condition::all()
|
||||
.add(get_current_schema(db).equals((
|
||||
InformationSchema::TableConstraints,
|
||||
InformationSchema::TableSchema,
|
||||
)))
|
||||
.add(
|
||||
Expr::col((
|
||||
InformationSchema::TableConstraints,
|
||||
InformationSchema::ConstraintType,
|
||||
))
|
||||
.eq("FOREIGN KEY"),
|
||||
),
|
||||
);
|
||||
stmt
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum PgType {
|
||||
Table,
|
||||
Oid,
|
||||
Typname,
|
||||
Typnamespace,
|
||||
Typelem,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum PgDepend {
|
||||
Table,
|
||||
Objid,
|
||||
Deptype,
|
||||
Refclassid,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum PgNamespace {
|
||||
Table,
|
||||
Oid,
|
||||
Nspname,
|
||||
}
|
||||
|
||||
fn query_pg_types<C>(db: &C) -> SelectStatement
|
||||
where
|
||||
C: ConnectionTrait,
|
||||
{
|
||||
Query::select()
|
||||
.column(PgType::Typname)
|
||||
.from(PgType::Table)
|
||||
.left_join(
|
||||
PgNamespace::Table,
|
||||
Expr::col((PgNamespace::Table, PgNamespace::Oid))
|
||||
.equals((PgType::Table, PgType::Typnamespace)),
|
||||
)
|
||||
.left_join(
|
||||
PgDepend::Table,
|
||||
Expr::col((PgDepend::Table, PgDepend::Objid))
|
||||
.equals((PgType::Table, PgType::Oid))
|
||||
.and(
|
||||
Expr::col((PgDepend::Table, PgDepend::Refclassid))
|
||||
.eq(Expr::cust("'pg_extension'::regclass::oid")),
|
||||
)
|
||||
.and(Expr::col((PgDepend::Table, PgDepend::Deptype)).eq(Expr::cust("'e'"))),
|
||||
)
|
||||
.and_where(get_current_schema(db).equals((PgNamespace::Table, PgNamespace::Nspname)))
|
||||
.and_where(Expr::col((PgType::Table, PgType::Typelem)).eq(0))
|
||||
.and_where(Expr::col((PgDepend::Table, PgDepend::Objid)).is_null())
|
||||
.take()
|
||||
}
|
||||
|
||||
trait QueryTable {
|
||||
type Statement;
|
||||
|
||||
fn table_name(self, table_name: DynIden) -> Self::Statement;
|
||||
}
|
||||
|
||||
impl QueryTable for SelectStatement {
|
||||
type Statement = SelectStatement;
|
||||
|
||||
fn table_name(mut self, table_name: DynIden) -> SelectStatement {
|
||||
self.from(table_name);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl QueryTable for sea_query::TableCreateStatement {
|
||||
type Statement = sea_query::TableCreateStatement;
|
||||
|
||||
fn table_name(mut self, table_name: DynIden) -> sea_query::TableCreateStatement {
|
||||
self.table(table_name);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> QueryTable for sea_orm::Insert<A>
|
||||
where
|
||||
A: ActiveModelTrait,
|
||||
{
|
||||
type Statement = sea_orm::Insert<A>;
|
||||
|
||||
fn table_name(mut self, table_name: DynIden) -> sea_orm::Insert<A> {
|
||||
sea_orm::QueryTrait::query(&mut self).into_table(table_name);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> QueryTable for sea_orm::DeleteMany<E>
|
||||
where
|
||||
E: EntityTrait,
|
||||
{
|
||||
type Statement = sea_orm::DeleteMany<E>;
|
||||
|
||||
fn table_name(mut self, table_name: DynIden) -> sea_orm::DeleteMany<E> {
|
||||
sea_orm::QueryTrait::query(&mut self).from_table(table_name);
|
||||
self
|
||||
}
|
||||
}
|
||||
611
extensions/pagetop-seaorm/src/migration/schema.rs
Normal file
611
extensions/pagetop-seaorm/src/migration/schema.rs
Normal file
|
|
@ -0,0 +1,611 @@
|
|||
//! Adaptación de <https://github.com/loco-rs/loco/blob/master/src/schema.rs>
|
||||
//!
|
||||
//! # Ayudantes de esquema de base de datos
|
||||
//!
|
||||
//! Define funciones y ayudantes para crear esquemas de tablas usando `sea-orm` y `sea-query`.
|
||||
//!
|
||||
//! # Ejemplo
|
||||
//!
|
||||
//! El siguiente ejemplo muestra cómo escribir un archivo de migración usando los ayudantes
|
||||
//! de esquema.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use pagetop_seaorm::migration::*;
|
||||
//!
|
||||
//! pub struct Migration;
|
||||
//!
|
||||
//! #[async_trait::async_trait]
|
||||
//! impl MigrationTrait for Migration {
|
||||
//! async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
//! let table = table_auto(Users::Table)
|
||||
//! .col(pk_auto(Users::Id))
|
||||
//! .col(uuid(Users::Pid))
|
||||
//! .col(string_uniq(Users::Email))
|
||||
//! .col(string(Users::Password))
|
||||
//! .col(string(Users::Name))
|
||||
//! .col(string_null(Users::ResetToken))
|
||||
//! .col(timestamp_null(Users::ResetSentAt))
|
||||
//! .to_owned();
|
||||
//! manager.create_table(table).await?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//!
|
||||
//! async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
//! manager
|
||||
//! .drop_table(Table::drop().table(Users::Table).to_owned())
|
||||
//! .await
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(DeriveIden)]
|
||||
//! pub enum Users {
|
||||
//! Table,
|
||||
//! Id,
|
||||
//! Pid,
|
||||
//! Email,
|
||||
//! Name,
|
||||
//! Password,
|
||||
//! ResetToken,
|
||||
//! ResetSentAt,
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use sea_orm::sea_query::{
|
||||
self, Alias, ColumnDef, ColumnType, Expr, Iden, IntoIden, PgInterval, Table,
|
||||
TableCreateStatement,
|
||||
};
|
||||
|
||||
#[derive(Iden)]
|
||||
enum GeneralIds {
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
|
||||
/// Wrapping table schema creation.
|
||||
pub fn table_auto<T: IntoIden + 'static>(name: T) -> TableCreateStatement {
|
||||
timestamps(Table::create().table(name).if_not_exists().take())
|
||||
}
|
||||
|
||||
/// Create a primary key column with auto-increment feature.
|
||||
pub fn pk_auto<T: IntoIden>(name: T) -> ColumnDef {
|
||||
integer(name).auto_increment().primary_key().take()
|
||||
}
|
||||
|
||||
/// Create a UUID primary key
|
||||
pub fn pk_uuid<T: IntoIden>(name: T) -> ColumnDef {
|
||||
uuid(name).primary_key().take()
|
||||
}
|
||||
|
||||
pub fn char_len<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||
ColumnDef::new(col).char_len(length).not_null().take()
|
||||
}
|
||||
|
||||
pub fn char_len_null<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||
ColumnDef::new(col).char_len(length).null().take()
|
||||
}
|
||||
|
||||
pub fn char_len_uniq<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||
char_len(col, length).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn char<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).char().not_null().take()
|
||||
}
|
||||
|
||||
pub fn char_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).char().null().take()
|
||||
}
|
||||
|
||||
pub fn char_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
char(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn string_len<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||
ColumnDef::new(col).string_len(length).not_null().take()
|
||||
}
|
||||
|
||||
pub fn string_len_null<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||
ColumnDef::new(col).string_len(length).null().take()
|
||||
}
|
||||
|
||||
pub fn string_len_uniq<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||
string_len(col, length).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn string<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).string().not_null().take()
|
||||
}
|
||||
|
||||
pub fn string_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).string().null().take()
|
||||
}
|
||||
|
||||
pub fn string_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
string(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn text<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).text().not_null().take()
|
||||
}
|
||||
|
||||
pub fn text_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).text().null().take()
|
||||
}
|
||||
|
||||
pub fn text_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
text(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn tiny_integer<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).tiny_integer().not_null().take()
|
||||
}
|
||||
|
||||
pub fn tiny_integer_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).tiny_integer().null().take()
|
||||
}
|
||||
|
||||
pub fn tiny_integer_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
tiny_integer(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn small_integer<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).small_integer().not_null().take()
|
||||
}
|
||||
|
||||
pub fn small_integer_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).small_integer().null().take()
|
||||
}
|
||||
|
||||
pub fn small_integer_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
small_integer(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn integer<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).integer().not_null().take()
|
||||
}
|
||||
|
||||
pub fn integer_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).integer().null().take()
|
||||
}
|
||||
|
||||
pub fn integer_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
integer(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn big_integer<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).big_integer().not_null().take()
|
||||
}
|
||||
|
||||
pub fn big_integer_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).big_integer().null().take()
|
||||
}
|
||||
|
||||
pub fn big_integer_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
big_integer(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn tiny_unsigned<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).tiny_unsigned().not_null().take()
|
||||
}
|
||||
|
||||
pub fn tiny_unsigned_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).tiny_unsigned().null().take()
|
||||
}
|
||||
|
||||
pub fn tiny_unsigned_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
tiny_unsigned(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn small_unsigned<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).small_unsigned().not_null().take()
|
||||
}
|
||||
|
||||
pub fn small_unsigned_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).small_unsigned().null().take()
|
||||
}
|
||||
|
||||
pub fn small_unsigned_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
small_unsigned(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn unsigned<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).unsigned().not_null().take()
|
||||
}
|
||||
|
||||
pub fn unsigned_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).unsigned().null().take()
|
||||
}
|
||||
|
||||
pub fn unsigned_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
unsigned(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn big_unsigned<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).big_unsigned().not_null().take()
|
||||
}
|
||||
|
||||
pub fn big_unsigned_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).big_unsigned().null().take()
|
||||
}
|
||||
|
||||
pub fn big_unsigned_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
big_unsigned(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn float<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).float().not_null().take()
|
||||
}
|
||||
|
||||
pub fn float_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).float().null().take()
|
||||
}
|
||||
|
||||
pub fn float_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
float(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn double<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).double().not_null().take()
|
||||
}
|
||||
|
||||
pub fn double_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).double().null().take()
|
||||
}
|
||||
|
||||
pub fn double_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
double(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn decimal_len<T: IntoIden>(col: T, precision: u32, scale: u32) -> ColumnDef {
|
||||
ColumnDef::new(col)
|
||||
.decimal_len(precision, scale)
|
||||
.not_null()
|
||||
.take()
|
||||
}
|
||||
|
||||
pub fn decimal_len_null<T: IntoIden>(col: T, precision: u32, scale: u32) -> ColumnDef {
|
||||
ColumnDef::new(col)
|
||||
.decimal_len(precision, scale)
|
||||
.null()
|
||||
.take()
|
||||
}
|
||||
|
||||
pub fn decimal_len_uniq<T: IntoIden>(col: T, precision: u32, scale: u32) -> ColumnDef {
|
||||
decimal_len(col, precision, scale).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn decimal<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).decimal().not_null().take()
|
||||
}
|
||||
|
||||
pub fn decimal_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).decimal().null().take()
|
||||
}
|
||||
|
||||
pub fn decimal_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
decimal(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn date_time<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).date_time().not_null().take()
|
||||
}
|
||||
|
||||
pub fn date_time_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).date_time().null().take()
|
||||
}
|
||||
|
||||
pub fn date_time_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
date_time(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn interval<T: IntoIden>(
|
||||
col: T,
|
||||
fields: Option<PgInterval>,
|
||||
precision: Option<u32>,
|
||||
) -> ColumnDef {
|
||||
ColumnDef::new(col)
|
||||
.interval(fields, precision)
|
||||
.not_null()
|
||||
.take()
|
||||
}
|
||||
|
||||
pub fn interval_null<T: IntoIden>(
|
||||
col: T,
|
||||
fields: Option<PgInterval>,
|
||||
precision: Option<u32>,
|
||||
) -> ColumnDef {
|
||||
ColumnDef::new(col)
|
||||
.interval(fields, precision)
|
||||
.null()
|
||||
.take()
|
||||
}
|
||||
|
||||
pub fn interval_uniq<T: IntoIden>(
|
||||
col: T,
|
||||
fields: Option<PgInterval>,
|
||||
precision: Option<u32>,
|
||||
) -> ColumnDef {
|
||||
interval(col, fields, precision).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn timestamp<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).timestamp().not_null().take()
|
||||
}
|
||||
|
||||
pub fn timestamp_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).timestamp().null().take()
|
||||
}
|
||||
|
||||
pub fn timestamp_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
timestamp(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn timestamp_with_time_zone<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null()
|
||||
.take()
|
||||
}
|
||||
|
||||
pub fn timestamp_with_time_zone_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).timestamp_with_time_zone().null().take()
|
||||
}
|
||||
|
||||
pub fn timestamp_with_time_zone_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
timestamp_with_time_zone(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn time<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).time().not_null().take()
|
||||
}
|
||||
|
||||
pub fn time_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).time().null().take()
|
||||
}
|
||||
|
||||
pub fn time_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
time(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn date<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).date().not_null().take()
|
||||
}
|
||||
|
||||
pub fn date_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).date().null().take()
|
||||
}
|
||||
|
||||
pub fn date_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
date(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn year<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).year().not_null().take()
|
||||
}
|
||||
|
||||
pub fn year_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).year().null().take()
|
||||
}
|
||||
|
||||
pub fn year_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
year(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn binary_len<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||
ColumnDef::new(col).binary_len(length).not_null().take()
|
||||
}
|
||||
|
||||
pub fn binary_len_null<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||
ColumnDef::new(col).binary_len(length).null().take()
|
||||
}
|
||||
|
||||
pub fn binary_len_uniq<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||
binary_len(col, length).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn binary<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).binary().not_null().take()
|
||||
}
|
||||
|
||||
pub fn binary_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).binary().null().take()
|
||||
}
|
||||
|
||||
pub fn binary_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
binary(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn var_binary<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||
ColumnDef::new(col).var_binary(length).not_null().take()
|
||||
}
|
||||
|
||||
pub fn var_binary_null<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||
ColumnDef::new(col).var_binary(length).null().take()
|
||||
}
|
||||
|
||||
pub fn var_binary_uniq<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||
var_binary(col, length).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn bit<T: IntoIden>(col: T, length: Option<u32>) -> ColumnDef {
|
||||
ColumnDef::new(col).bit(length).not_null().take()
|
||||
}
|
||||
|
||||
pub fn bit_null<T: IntoIden>(col: T, length: Option<u32>) -> ColumnDef {
|
||||
ColumnDef::new(col).bit(length).null().take()
|
||||
}
|
||||
|
||||
pub fn bit_uniq<T: IntoIden>(col: T, length: Option<u32>) -> ColumnDef {
|
||||
bit(col, length).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn varbit<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||
ColumnDef::new(col).varbit(length).not_null().take()
|
||||
}
|
||||
|
||||
pub fn varbit_null<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||
ColumnDef::new(col).varbit(length).null().take()
|
||||
}
|
||||
|
||||
pub fn varbit_uniq<T: IntoIden>(col: T, length: u32) -> ColumnDef {
|
||||
varbit(col, length).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn blob<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).blob().not_null().take()
|
||||
}
|
||||
|
||||
pub fn blob_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).blob().null().take()
|
||||
}
|
||||
|
||||
pub fn blob_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
blob(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn boolean<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).boolean().not_null().take()
|
||||
}
|
||||
|
||||
pub fn boolean_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).boolean().null().take()
|
||||
}
|
||||
|
||||
pub fn boolean_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
boolean(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn money_len<T: IntoIden>(col: T, precision: u32, scale: u32) -> ColumnDef {
|
||||
ColumnDef::new(col)
|
||||
.money_len(precision, scale)
|
||||
.not_null()
|
||||
.take()
|
||||
}
|
||||
|
||||
pub fn money_len_null<T: IntoIden>(col: T, precision: u32, scale: u32) -> ColumnDef {
|
||||
ColumnDef::new(col)
|
||||
.money_len(precision, scale)
|
||||
.null()
|
||||
.take()
|
||||
}
|
||||
|
||||
pub fn money_len_uniq<T: IntoIden>(col: T, precision: u32, scale: u32) -> ColumnDef {
|
||||
money_len(col, precision, scale).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn money<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).money().not_null().take()
|
||||
}
|
||||
|
||||
pub fn money_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).money().null().take()
|
||||
}
|
||||
|
||||
pub fn money_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
money(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn json<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).json().not_null().take()
|
||||
}
|
||||
|
||||
pub fn json_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).json().null().take()
|
||||
}
|
||||
|
||||
pub fn json_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
json(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn json_binary<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).json_binary().not_null().take()
|
||||
}
|
||||
|
||||
pub fn json_binary_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).json_binary().null().take()
|
||||
}
|
||||
|
||||
pub fn json_binary_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
json_binary(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn uuid<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).uuid().not_null().take()
|
||||
}
|
||||
|
||||
pub fn uuid_null<T: IntoIden>(col: T) -> ColumnDef {
|
||||
ColumnDef::new(col).uuid().null().take()
|
||||
}
|
||||
|
||||
pub fn uuid_uniq<T: IntoIden>(col: T) -> ColumnDef {
|
||||
uuid(col).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn custom<T: IntoIden, N: IntoIden>(col: T, name: N) -> ColumnDef {
|
||||
ColumnDef::new(col).custom(name).not_null().take()
|
||||
}
|
||||
|
||||
pub fn custom_null<T: IntoIden, N: IntoIden>(col: T, name: N) -> ColumnDef {
|
||||
ColumnDef::new(col).custom(name).null().take()
|
||||
}
|
||||
|
||||
pub fn enumeration<T, N, S, V>(col: T, name: N, variants: V) -> ColumnDef
|
||||
where
|
||||
T: IntoIden,
|
||||
N: IntoIden,
|
||||
S: IntoIden,
|
||||
V: IntoIterator<Item = S>,
|
||||
{
|
||||
ColumnDef::new(col)
|
||||
.enumeration(name, variants)
|
||||
.not_null()
|
||||
.take()
|
||||
}
|
||||
|
||||
pub fn enumeration_null<T, N, S, V>(col: T, name: N, variants: V) -> ColumnDef
|
||||
where
|
||||
T: IntoIden,
|
||||
N: IntoIden,
|
||||
S: IntoIden,
|
||||
V: IntoIterator<Item = S>,
|
||||
{
|
||||
ColumnDef::new(col)
|
||||
.enumeration(name, variants)
|
||||
.null()
|
||||
.take()
|
||||
}
|
||||
|
||||
pub fn enumeration_uniq<T, N, S, V>(col: T, name: N, variants: V) -> ColumnDef
|
||||
where
|
||||
T: IntoIden,
|
||||
N: IntoIden,
|
||||
S: IntoIden,
|
||||
V: IntoIterator<Item = S>,
|
||||
{
|
||||
enumeration(col, name, variants).unique_key().take()
|
||||
}
|
||||
|
||||
pub fn array<T: IntoIden>(col: T, elem_type: ColumnType) -> ColumnDef {
|
||||
ColumnDef::new(col).array(elem_type).not_null().take()
|
||||
}
|
||||
|
||||
pub fn array_null<T: IntoIden>(col: T, elem_type: ColumnType) -> ColumnDef {
|
||||
ColumnDef::new(col).array(elem_type).null().take()
|
||||
}
|
||||
|
||||
pub fn array_uniq<T: IntoIden>(col: T, elem_type: ColumnType) -> ColumnDef {
|
||||
array(col, elem_type).unique_key().take()
|
||||
}
|
||||
|
||||
/// Añade las columnas de timestamp (`CreatedAt` y `UpdatedAt`) a una tabla existente.
|
||||
pub fn timestamps(t: TableCreateStatement) -> TableCreateStatement {
|
||||
let mut t = t;
|
||||
t.col(timestamp(GeneralIds::CreatedAt).default(Expr::current_timestamp()))
|
||||
.col(timestamp(GeneralIds::UpdatedAt).default(Expr::current_timestamp()))
|
||||
.take()
|
||||
}
|
||||
|
||||
/// Crea un alias.
|
||||
pub fn name<T: Into<String>>(name: T) -> Alias {
|
||||
Alias::new(name)
|
||||
}
|
||||
15
extensions/pagetop-seaorm/src/migration/seaql_migrations.rs
Normal file
15
extensions/pagetop-seaorm/src/migration/seaql_migrations.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
// One should override the name of migration table via `MigratorTrait::migration_table_name` method
|
||||
#[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 {}
|
||||
|
|
@ -65,6 +65,7 @@ case "$CRATE" in
|
|||
# Extensions
|
||||
--exclude-path "extensions/pagetop-aliner/**/*"
|
||||
--exclude-path "extensions/pagetop-bootsier/**/*"
|
||||
--exclude-path "extensions/pagetop-seaorm/**/*"
|
||||
)
|
||||
;;
|
||||
pagetop-aliner)
|
||||
|
|
@ -75,6 +76,10 @@ case "$CRATE" in
|
|||
CHANGELOG_FILE="extensions/pagetop-bootsier/CHANGELOG.md"
|
||||
PATH_FLAGS=(--include-path "extensions/pagetop-bootsier/**/*")
|
||||
;;
|
||||
pagetop-seaorm)
|
||||
CHANGELOG_FILE="extensions/pagetop-seaorm/CHANGELOG.md"
|
||||
PATH_FLAGS=(--include-path "extensions/pagetop-seaorm/**/*")
|
||||
;;
|
||||
*)
|
||||
echo "Error: unsupported crate '$CRATE'" >&2
|
||||
exit 1
|
||||
|
|
@ -120,7 +125,9 @@ read -r -p "Do you want to proceed with the release of $CRATE? [y/N] " REPLY
|
|||
echo ""
|
||||
if [[ ! "$REPLY" =~ ^[Yy]$ ]]; then
|
||||
echo "Aborting release process." >&2
|
||||
if [[ -n "${PAGETOP_RESTORE_TREE:-}" ]]; then
|
||||
git restore --worktree -- .
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ cd "$(dirname "$0")/.." || exit 1
|
|||
# ------------------------------------------------------------------------------
|
||||
# DRY-RUN (por defecto) o ejecución real con --execute
|
||||
# ------------------------------------------------------------------------------
|
||||
export PAGETOP_RESTORE_TREE=1
|
||||
|
||||
if [[ "$EXECUTE" != "--execute" ]]; then
|
||||
echo "Running dry-run (default mode). Add --execute to publish"
|
||||
cargo release --config "$CONFIG" --package "$CRATE" "$LEVEL"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue