ferro-cli 0.2.24

CLI for scaffolding Ferro web applications
Documentation
---
name: ferro:model
description: Generate a new model with migration
allowed-tools:
  - Bash
  - Read
  - Write
  - Glob
  - AskUserQuestion
---

<objective>
Generate a new Ferro model with corresponding database migration.

Creates:
1. Model file in `src/models/{name}.rs`
2. Entity file in `src/models/entities/{name}.rs` (if using db:sync)
3. Migration file in `migrations/`
4. Updates `src/models/mod.rs` to include the new model
</objective>

<arguments>
Required:
- `name` - Model name in PascalCase (e.g., `Post`, `UserProfile`)

Optional:
- `fields` - Field definitions in `name:type` format
  - Types: string, text, integer, bigint, boolean, datetime, date, decimal, json, uuid

Examples:
- `/ferro:model Post`
- `/ferro:model Post title:string body:text published:boolean`
- `/ferro:model Comment post_id:integer user_id:integer body:text`
</arguments>

<process>

<step name="parse_args">

Parse the model name and fields from arguments.

If no fields provided, ask:
```
What fields should this model have?

Common patterns:
- Blog post: title:string body:text slug:string published_at:datetime
- User profile: user_id:integer bio:text avatar:string
- Product: name:string description:text price:decimal stock:integer
```

</step>

<step name="check_existing">

Check if model already exists:

```bash
if [ -f "src/models/{snake_name}.rs" ]; then
    echo "EXISTS"
fi
```

If exists, ask whether to overwrite or abort.

</step>

<step name="generate_migration">

Generate migration file with timestamp:

```bash
TIMESTAMP=$(date +%Y%m%d%H%M%S)
MIGRATION_NAME="${TIMESTAMP}_create_{table_name}_table"
```

Migration content:
```rust
use sea_orm_migration::prelude::*;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .create_table(
                Table::create()
                    .table({TableName}::Table)
                    .if_not_exists()
                    .col(ColumnDef::new({TableName}::Id).integer().not_null().auto_increment().primary_key())
                    {field_columns}
                    .col(ColumnDef::new({TableName}::CreatedAt).timestamp().not_null().default(Expr::current_timestamp()))
                    .col(ColumnDef::new({TableName}::UpdatedAt).timestamp().not_null().default(Expr::current_timestamp()))
                    .to_owned(),
            )
            .await
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager.drop_table(Table::drop().table({TableName}::Table).to_owned()).await
    }
}

#[derive(DeriveIden)]
enum {TableName} {
    Table,
    Id,
    {field_idents}
    CreatedAt,
    UpdatedAt,
}
```

</step>

<step name="generate_model">

Generate model file at `src/models/{snake_name}.rs`:

```rust
//! {ModelName} model
//!
//! This file contains custom implementations for the {ModelName} model.
//! The base entity is auto-generated in src/models/entities/{snake_name}.rs
//!
//! This file is NEVER overwritten by `ferro db:sync` - your custom code is safe here.

// Re-export the auto-generated entity
pub use super::entities::{snake_name}::*;

/// Type alias for convenient access
#[allow(dead_code)]
pub type {ModelName} = Model;

// ============================================================================
// CUSTOM METHODS
// Add your custom query and mutation methods below
// ============================================================================

// impl Model {
//     pub async fn find_by_slug(slug: &str) -> Result<Option<Self>, ferro::FrameworkError> {
//         Self::query().filter(Column::Slug.eq(slug)).first().await
//     }
// }

// ============================================================================
// RELATIONS
// Define relationships to other entities here
// ============================================================================

// impl Entity {
//     pub fn belongs_to_user() -> RelationDef {
//         Entity::belongs_to(super::users::Entity)
//             .from(Column::UserId)
//             .to(super::users::Column::Id)
//             .into()
//     }
// }
```

</step>

<step name="update_mod">

Update `src/models/mod.rs` to include the new model:

```rust
pub mod {snake_name};
```

</step>

<step name="summary">

Output summary:
```
Created model: {ModelName}

Files created:
  - src/models/{snake_name}.rs
  - migrations/{timestamp}_create_{table_name}_table.rs

Next steps:
  1. Run migration: ferro db:migrate (or cargo run -- db:migrate)
  2. Sync entities: ferro db:sync (generates src/models/entities/{snake_name}.rs)
  3. Add relationships in src/models/{snake_name}.rs
```

</step>

</process>

<field_type_mapping>
| Input Type | Rust Type      | SeaORM Column              |
|------------|----------------|----------------------------|
| string     | String         | ColumnDef::string()        |
| text       | String         | ColumnDef::text()          |
| integer    | i32            | ColumnDef::integer()       |
| bigint     | i64            | ColumnDef::big_integer()   |
| boolean    | bool           | ColumnDef::boolean()       |
| datetime   | DateTimeUtc    | ColumnDef::timestamp()     |
| date       | Date           | ColumnDef::date()          |
| decimal    | Decimal        | ColumnDef::decimal()       |
| json       | Json           | ColumnDef::json()          |
| uuid       | Uuid           | ColumnDef::uuid()          |
</field_type_mapping>