alibi-cli 0.7.0

Schema generator for Alibi authentication
use crate::parse_type;
use crate::selection::{CORE_ENTITIES, Selection, entity_fields, role_name};
use alibi_schema_registry::{EntityRole, FieldDef};
use proc_macro2::TokenStream;
use quote::{format_ident, quote};

pub(crate) fn seaorm_schema(selection: &Selection) -> TokenStream {
    let core_entities = CORE_ENTITIES.iter().map(|(module, table, role)| {
        seaorm_entity(module, table, Some(*role), &entity_fields(selection, *role))
    });
    let extra_entities = selection.extra.iter().map(|entity| {
        seaorm_entity(
            entity.mod_name,
            entity.table_name,
            entity.role,
            &entity.fields.iter().collect::<Vec<_>>(),
        )
    });
    let mut tables = CORE_ENTITIES
        .iter()
        .map(|(_, table, role)| (*table, entity_fields(selection, *role)))
        .chain(
            selection
                .extra
                .iter()
                .map(|entity| (entity.table_name, entity.fields.iter().collect())),
        )
        .collect::<Vec<_>>();
    tables.sort_by_key(|(table, _)| crate::sqlx::table_order(table));
    let sqlite = tables.iter().flat_map(|(table, fields)| {
        crate::sqlx::migration_statements(table, fields, crate::sqlx::SqlDialect::Sqlite)
    });
    let postgres = tables.iter().flat_map(|(table, fields)| {
        crate::sqlx::migration_statements(table, fields, crate::sqlx::SqlDialect::Postgres)
    });

    quote! {
        use alibi::AuthSchema;
        use alibi::seaorm::sea_orm;
        use alibi::seaorm::sea_orm::entity::prelude::*;
        use alibi::seaorm::sea_orm::ConnectionTrait;
        use alibi::seaorm::{AuthEntity, DatabaseConnection};

        #(#core_entities)*
        #(#extra_entities)*

        pub struct AppAuthSchema;

        impl AuthSchema for AppAuthSchema {
            type User = user::Model;
            type Session = session::Model;
            type Account = account::Model;
            type Verification = verification::Model;
        }

        pub async fn run_app_migrations(
            database: &DatabaseConnection,
        ) -> Result<(), sea_orm::DbErr> {
            let statements: &[&str] = match database.get_database_backend() {
                sea_orm::DbBackend::Sqlite => &[#(#sqlite),*],
                sea_orm::DbBackend::Postgres => &[#(#postgres),*],
                _ => return Err(sea_orm::DbErr::Custom("Generated migrations support SQLite and PostgreSQL".into())),
            };
            for statement in statements {
                let _ = database.execute_unprepared(statement).await?;
            }
            Ok(())
        }
    }
}

fn seaorm_entity(
    module: &str,
    table: &str,
    role: Option<EntityRole>,
    fields: &[&FieldDef],
) -> TokenStream {
    let module = format_ident!("{}", module);
    let fields = fields.iter().map(|field| {
        let name = format_ident!("{}", field.name);
        let ty = parse_type(
            field,
            &field.ty.replace("f64", "alibi::seaorm::Number").replace(
                "i64",
                if field.name == "last_request" {
                    "i64"
                } else {
                    "alibi::seaorm::Integer"
                },
            ),
        );
        let column_attr = field
            .column_name
            .map(|column| quote! { #[sea_orm(column_name = #column)] });
        let default_attr = field
            .default_value
            .map(|value| quote! { #[sea_orm(default_value = #value)] });
        let primary_key = field
            .is_primary_key
            .then(|| quote! { #[sea_orm(primary_key, auto_increment = false)] });
        let storage = (table == "organization" && field.name == "metadata")
            .then(|| quote! { #[sea_orm(column_type = "Text", save_as = "text")] });
        quote! {
            #storage
            #column_attr
            #default_attr
            #primary_key
            pub #name: #ty,
        }
    });
    let derive = role.map_or_else(
        || quote! { #[derive(Clone, Debug, serde::Serialize, DeriveEntityModel)] },
        |role| {
            let role = role_name(role);
            quote! {
                #[derive(Clone, Debug, serde::Serialize, DeriveEntityModel, AuthEntity)]
                #[auth(role = #role)]
            }
        },
    );
    quote! {
        mod #module {
            use super::*;

            #derive
            #[sea_orm(table_name = #table)]
            pub struct Model {
                #(#fields)*
            }

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

            impl ActiveModelBehavior for ActiveModel {}
        }
    }
}