Files
MeowSpool/backend/agent.md
T
Felipe abdc2fe8ce feat: implement filament and spool preset services with CRUD operations
- Add `FilamentService` for managing filament inventory, including creation, retrieval, updating, and deletion of filaments.
- Introduce `SpoolPresetService` for handling spool presets, allowing users to create, update, and delete their custom presets.
- Create domain models for `Filament` and `SpoolPreset` with necessary fields and methods.
- Define repository interfaces for filament and spool preset persistence.
- Implement application configuration management from environment variables.
- Set up error handling with a centralized `AppError` type.
- Build the Axum router with public and protected routes for user authentication and resource management.
2026-03-14 09:38:28 -03:00

14 KiB

MeowSpool Backend — Agent Guide

Visão Geral

Backend da aplicação MeowSpool escrito em Rust, utilizando Axum como framework HTTP e SQLx com PostgreSQL como banco de dados. A arquitetura segue o padrão Hexagonal (Ports & Adapters), garantindo que o núcleo do domínio seja completamente isolado de detalhes de infraestrutura.


Stack

Componente Biblioteca Versão mínima
HTTP Framework axum 0.7
Async Runtime tokio (full) 1.x
ORM / Query sqlx (postgres, uuid, time, macros) 0.8
Autenticação JWT jsonwebtoken 9.x
Hash de senha argon2 0.5
OAuth Google oauth2 4.x
UUID uuid (v4, serde) 1.x
Serialização serde, serde_json 1.x
Erros thiserror 1.x
Env vars dotenvy 0.15
Logging tracing, tracing-subscriber 0.1
Validação validator 0.18
HTTP Client reqwest (json, rustls-tls) 0.12
Geração QR Code qrcode 0.14

Estrutura de Pastas

backend/
├── Cargo.toml
├── Cargo.lock
├── .env.example
├── agent.md                           <- este arquivo
├── migrations/                        <- SQL puro, gerenciado pelo SQLx CLI
│   ├── 20240101000001_create_users.sql
│   ├── 20240101000002_create_spool_presets.sql
│   └── 20240101000003_create_filaments.sql
└── src/
    ├── main.rs                        <- entry point: inicializa config, DB, router e servidor
    ├── config.rs                      <- struct Config lida de variáveis de ambiente
    ├── error.rs                       <- AppError unificado com IntoResponse
    ├── router.rs                      <- composição de todas as rotas Axum
    │
    ├── domain/                        <- NÚCLEO: entidades puras, sem dependências externas
    │   ├── mod.rs
    │   ├── user.rs                    <- struct User, enum AuthProvider
    │   ├── filament.rs                <- struct Filament, enum Material
    │   └── spool_preset.rs            <- struct SpoolPreset
    │
    ├── ports/                         <- INTERFACES: traits que o domínio exige
    │   ├── mod.rs
    │   ├── user_repository.rs         <- trait UserRepository
    │   ├── filament_repository.rs     <- trait FilamentRepository
    │   └── spool_preset_repository.rs <- trait SpoolPresetRepository
    │
    ├── application/                   <- CASOS DE USO: orquestram domínio + ports
    │   ├── mod.rs
    │   ├── auth_service.rs            <- login, register, OAuth, refresh, logout
    │   ├── filament_service.rs        <- CRUD, cálculo de peso líquido, QR, SVG
    │   └── spool_preset_service.rs    <- CRUD presets (system read-only, user CRUD)
    │
    └── adapters/
        ├── inbound/                   <- HTTP: recebe requisições, delega ao application
        │   ├── mod.rs
        │   ├── auth_handler.rs
        │   ├── filament_handler.rs
        │   ├── spool_preset_handler.rs
        │   ├── user_handler.rs
        │   └── middleware/
        │       └── auth.rs            <- extrator JWT que popula CurrentUser no estado
        └── outbound/                  <- INFRAESTRUTURA: implementa as traits de ports
            ├── mod.rs
            ├── postgres_user_repo.rs
            ├── postgres_filament_repo.rs
            └── postgres_spool_preset_repo.rs

Arquitetura Hexagonal — Regras de Dependência

adapters/inbound (HTTP)
        |
        v
  application (services)
        |
        v
    domain (entidades)
        |
        ^
    ports (traits)
        |
        ^
adapters/outbound (PostgreSQL)

Regra fundamental: O domain e os ports nunca importam nada de adapters ou application. Dependências permitidas no domínio: uuid, serde, time. Qualquer violação é um bug arquitetural.


Rotas da API

Todas as rotas são prefixadas com /api/v1.

Auth — /api/v1/auth

Método Rota Handler Acesso
POST /register register_handler Público
POST /login login_handler Público
POST /oauth/google google_oauth_handler Público
POST /refresh refresh_token_handler Público (requer refresh token)
POST /logout logout_handler Autenticado
POST /forgot-password forgot_password_handler Público
POST /verify-email verify_email_handler Público
POST /reset-password reset_password_handler Público (requer token de reset)

Users — /api/v1/users

Método Rota Handler Acesso
GET /me get_me_handler Autenticado
PUT /me update_me_handler Autenticado

Filaments — /api/v1/filaments

Método Rota Handler Acesso
GET / list_filaments_handler Autenticado
POST / create_filament_handler Autenticado
GET /:id get_filament_handler Autenticado
PUT /:id update_filament_handler Autenticado
DELETE /:id delete_filament_handler Autenticado
GET /:id/qrcode get_qrcode_handler Autenticado
GET /:id/label.svg export_label_handler Autenticado

Query params de listagem (GET /filaments):

  • material — filtra por tipo (PLA, ABS, PETG, TPU, ASA, PA, PC...)
  • brand — filtra por marca
  • search — busca em marca, modelo e notas
  • stock_levellow (<=15%), medium (<=35%), ok (>35%)
  • sortnet_weight_asc, net_weight_desc, created_at_desc (padrão)
  • page / per_page — paginação (padrão: página 1, 20 itens)

Spool Presets — /api/v1/spool-presets

Método Rota Handler Acesso
GET / list_presets_handler Autenticado
POST / create_preset_handler Autenticado
PUT /:id update_preset_handler Autenticado (apenas presets do user)
DELETE /:id delete_preset_handler Autenticado (apenas presets do user)

Dashboard — /api/v1/dashboard

Método Rota Handler Acesso
GET / dashboard_handler Autenticado

Resposta do dashboard:

{
  "total_stock_kg": 14.2,
  "low_stock_count": 3,
  "by_material": [
    { "material": "PLA", "count": 4, "total_kg": 5.8 }
  ],
  "low_stock_filaments": [
    { "id": "...", "model": "PETG", "net_weight_g": 120, "percentage": 12 }
  ],
  "recent_filaments": [...]
}

Padrão de Erros

Use thiserror em todos os módulos e um AppError central em src/error.rs:

#[derive(Debug, thiserror::Error)]
pub enum AppError {
    #[error("not found")]
    NotFound,

    #[error("unauthorized")]
    Unauthorized,

    #[error("forbidden")]
    Forbidden,

    #[error("validation error: {0}")]
    Validation(String),

    #[error("conflict: {0}")]
    Conflict(String),

    #[error("internal error")]
    Internal(#[from] anyhow::Error),
}

AppError implementa IntoResponse do Axum, mapeando cada variante para o status HTTP correto e body JSON consistente:

{ "error": "not found", "code": "NOT_FOUND" }

Convenções de Código

Nomenclatura

  • Structs de domínio: PascalCaseUser, Filament, SpoolPreset
  • Traits (ports): PascalCase com sufixo RepositoryUserRepository
  • Implementações concretas: prefixo do banco — PostgresUserRepository
  • Serviços: sufixo ServiceAuthService, FilamentService
  • Handlers: sufixo _handlerlogin_handler, create_filament_handler
  • DTOs de entrada: sufixo RequestCreateFilamentRequest
  • DTOs de saída: sufixo ResponseFilamentResponse

Estrutura de um Handler

Todo handler deve ser uma função async pequena. A lógica de negócio nunca fica no handler — ela fica no application/*_service.rs.

// adapters/inbound/filament_handler.rs
pub async fn create_filament_handler(
    State(state): State<AppState>,
    Extension(current_user): Extension<CurrentUser>,
    Json(req): Json<CreateFilamentRequest>,
) -> Result<impl IntoResponse, AppError> {
    req.validate()?;
    let filament = state.filament_service.create(current_user.id, req).await?;
    Ok((StatusCode::CREATED, Json(FilamentResponse::from(filament))))
}

Estrutura de um Service

O service recebe repositórios via injeção de dependência (trait objects em Arc):

// application/filament_service.rs
pub struct FilamentService {
    repo: Arc<dyn FilamentRepository>,
    preset_repo: Arc<dyn SpoolPresetRepository>,
}

impl FilamentService {
    pub async fn create(&self, user_id: Uuid, req: CreateFilamentRequest) -> Result<Filament, AppError> {
        // 1. buscar preset para calcular net_weight
        // 2. calcular net_weight_g = total_weight_g - spool_weight_g
        // 3. construir entidade Filament
        // 4. persistir via repo
        // 5. retornar entidade
    }
}

Estrutura de um Repository (Port)

// ports/filament_repository.rs
#[async_trait::async_trait]
pub trait FilamentRepository: Send + Sync {
    async fn find_by_id(&self, id: Uuid, user_id: Uuid) -> Result<Option<Filament>, AppError>;
    async fn list(&self, user_id: Uuid, filter: FilamentFilter) -> Result<Vec<Filament>, AppError>;
    async fn create(&self, filament: &Filament) -> Result<Filament, AppError>;
    async fn update(&self, filament: &Filament) -> Result<Filament, AppError>;
    async fn delete(&self, id: Uuid, user_id: Uuid) -> Result<(), AppError>;
}

Migrations

Use o SQLx CLI para gerenciar migrations:

# instalar
cargo install sqlx-cli --no-default-features --features postgres

# criar nova migration
sqlx migrate add <nome_descritivo>

# aplicar
sqlx migrate run

# reverter
sqlx migrate revert

Arquivos ficam em backend/migrations/. Nomeie com timestamp e descrição clara.


Variáveis de Ambiente

Copie .env.example para .env antes de rodar. Veja o arquivo .env.example para a lista completa.


Como Adicionar um Novo Endpoint (Passo a Passo)

  1. Domain — adicione ou modifique a entidade em src/domain/.
  2. Port — adicione o método necessário no trait em src/ports/.
  3. Outbound Adapter — implemente o método no repositório PostgreSQL em src/adapters/outbound/.
  4. Application Service — adicione o caso de uso em src/application/, chamando o port.
  5. Request/Response DTOs — defina structs com serde e validator no handler.
  6. Inbound Handler — crie o handler em src/adapters/inbound/, delegando para o service.
  7. Router — registre a rota em src/router.rs.
  8. Migration — se necessário, crie um arquivo SQL em migrations/.

Como Rodar Localmente

# na raiz do backend/
cp .env.example .env
# edite .env com suas credenciais locais

# subir PostgreSQL via Docker
docker run -d \
  --name meowspool-db \
  -e POSTGRES_USER=meowspool \
  -e POSTGRES_PASSWORD=meowspool \
  -e POSTGRES_DB=meowspool \
  -p 5432:5432 \
  postgres:16-alpine

# rodar migrations
sqlx migrate run

# rodar em modo watch (requer cargo-watch)
cargo watch -x run

Sincronização Offline (Estratégia)

O backend implementa last-write-wins com timestamp:

  • Toda entidade possui updated_at (timestamp UTC).
  • O mobile envia o updated_at local no body do PUT.
  • Se o updated_at do servidor for mais recente, retorna 409 Conflict com a versão do servidor.
  • Se o updated_at do cliente for mais recente (ou igual), o servidor aceita a atualização.

Geração de QR Code e Etiqueta SVG

QR Code (GET /filaments/:id/qrcode)

  • Gera QR Code com deep link: meowspool://filaments/:id
  • Retorna PNG (image/png) por padrão, ou SVG com ?format=svg
  • Biblioteca: crate qrcode

Etiqueta SVG (GET /filaments/:id/label.svg)

  • Query params: width_mm (padrão: 50), height_mm (padrão: 30)
  • Retorna SVG com: cor visual, modelo, material, marca, peso líquido e QR Code embutido
  • Content-Type: image/svg+xml
  • Content-Disposition: attachment; filename="meowspool-label-{id}.svg"

Segurança

  • Senhas hasheadas com Argon2id (crate argon2).
  • JWT assinado com HS256 — nunca exponha o JWT_SECRET.
  • Toda rota autenticada passa pelo middleware auth.rs que valida o token e popula Extension<CurrentUser>.
  • Queries usam bind parameters do SQLx — nunca interpolação de string em SQL.
  • user_id é sempre extraído do token JWT, nunca aceito como parâmetro de URL ou body.
  • Presets do sistema (is_system = true) são protegidos no nível de serviço: edição ou deleção retorna 403 Forbidden.