sync
This commit is contained in:
parent
48f640b55d
commit
440a2fa01f
28 changed files with 1849 additions and 96 deletions
341
00-work/doc.md
Normal file
341
00-work/doc.md
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
# Понимание проекта Trails Server: Архитектурный разбор и гайд для контрибьютора
|
||||
|
||||
## 1. Философия архитектуры: Почему именно такая структура?
|
||||
|
||||
Проект следует принципам **Clean Architecture** (чистая архитектура) с четким разделением ответственности. Каждый crate — это слой с собственной причиной существования:
|
||||
|
||||
- **domain** изолирует бизнес-правила от деталей реализации (ORM, HTTP, БД)
|
||||
- **entity** определяет data access objects (DAO), привязанные к SeaORM
|
||||
- **infrastructure** содержит "грязные" детали: репозитории, внешние сервисы, шаблоны email
|
||||
- **api-core** реализует бизнес-логику (use cases), зависящую только от domain
|
||||
- **api** — чистый адаптер HTTP, преобразующий запросы/ответы, но не содержащий логики
|
||||
- **bin** — entrypoint, который wire'ит все компоненты вместе
|
||||
|
||||
Эта структура позволяет:
|
||||
- **Тестировать бизнес-логику без HTTP/БД** (mock'ая repository)
|
||||
- **Менять технологии без рефакторинга** (например, заменить PostgreSQL на SQLite для тестов)
|
||||
- **Разным бинарникам использовать одну логику** (admin и client)
|
||||
|
||||
---
|
||||
|
||||
## 2. Обзор ключевых концепций домена
|
||||
|
||||
### "Trail" — центральная сущность
|
||||
На основе файлов миграций и структур, Trail — это контент-объект с жизненным циклом:
|
||||
- **Статус**: `draft` → `public` (см. миграцию `m20250129_122341`)
|
||||
- **Иерархия**: может быть root-элементом или child'ом (см. `m20241020_153507`)
|
||||
- **Thread**: цепочка связанных trails (соединение через `thread_id`)
|
||||
- **Content types**: поддержка множественных типов контента (`m20250618_120032`)
|
||||
- **Аналитика**: история посещений (`trails_visit_history`)
|
||||
- **Взаимодействие**: система лайков (`like.rs`)
|
||||
|
||||
### Abuse System
|
||||
Модерация с многоуровневой системой репортов (degree 1/2) и причинами (`abuse_reason.rs`)
|
||||
|
||||
### Два API в одном сервисе
|
||||
- **Client API** (`/client/*`): user-facing endpoints для создания trails, авторизации, профилей
|
||||
- **Admin API** (`/admin/*`): administrative endpoints для модерации, статистики, управления пользователями
|
||||
|
||||
---
|
||||
|
||||
## 3. Путешествие запроса: От TCP-сокета до БД
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ bin::client │
|
||||
│ └── tokio::main() │
|
||||
│ └── axum::Server::bind() │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ api::router::client::create_router() │
|
||||
│ └── routes like POST /trails │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ api::endpoints::client::trails::create_trail() │
|
||||
│ ├── Extractors (JWT, FormData) │
|
||||
│ └── Валидация через validator crate │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ api_core::trails::TrailsService::create() │
|
||||
│ └── Бизнес-правила: проверка прав, валидация статуса │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ infrastructure::repositories::trail::TrailRepository │
|
||||
│ └── sea_orm::Entity::insert() (database abstraction) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ entity::trail::ActiveModel → PostgreSQL │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Ключевой принцип**: `api` crate **не знает** о существовании БД. Он работает с абстракциями (`api-core` traits).
|
||||
|
||||
---
|
||||
|
||||
## 4. Глубокое погружение в crates
|
||||
|
||||
### `entity` — Data Access Layer
|
||||
- **Содержит**: SeaORM entity definitions, связанные 1:1 с таблицами БД
|
||||
- **Важно**: Все поля `pub`, но это нормально — это DTO для ORM
|
||||
- **Пример**: `user.rs` содержит `#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]`
|
||||
- **Использование**: Никогда не используйте `entity` в бизнес-логике напрямую. Всегда маппайте в `domain`
|
||||
|
||||
### `domain` — Language of the Business
|
||||
- **request/response**: DTO для API (разделены на `client`/`admin`)
|
||||
- **common**: Переиспользуемые типы (пагинация, токены, abuse)
|
||||
- **Валидация**: Derive-метки `#[derive(Validate)]` — central source of truth
|
||||
- **Пример**: `domain::client::trails::CreateTrailRequest` с валидацией полей
|
||||
|
||||
### `infrastructure` — "Грязные детали"
|
||||
**Репозитории** (`repositories/`):
|
||||
```rust
|
||||
// Паттерн: каждая сущность имеет trait + impl
|
||||
#[async_trait]
|
||||
pub trait TrailRepository: Send + Sync {
|
||||
async fn create(&self, trail: domain::trails::NewTrail) -> Result<TrailId>;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
Реализация зависит от `sea_orm`, но домен ничего не знает об этом.
|
||||
|
||||
**Services** (`services/`):
|
||||
- `auth.rs`: JWT encoding/decoding, bcrypt/argon2
|
||||
- `mail.rs`: Askama templates (type-safe email HTML)
|
||||
- `storage/`: Абстракция над файловым хранилищем через OpenDAL с **стратегиями**:
|
||||
- `single`: простое хранение
|
||||
- `mirror`: дублирование в несколько бакетов
|
||||
- `backup`: асинхронный бэкап в cold storage
|
||||
|
||||
**Forms** (`forms/`): Type-safe multipart form handling (`axum_typed_multipart`)
|
||||
|
||||
### `api-core` — Use Cases Orchestrator
|
||||
Содержит **stateless services**, которые координируют репозитории:
|
||||
```rust
|
||||
pub struct TrailService {
|
||||
trail_repo: Arc<dyn TrailRepository>,
|
||||
user_repo: Arc<dyn UserRepository>,
|
||||
storage: Arc<dyn StorageService>,
|
||||
}
|
||||
```
|
||||
Здесь проверяются сложные инварианты: права доступа, консистентность данных, транзакционность.
|
||||
|
||||
### `api` — HTTP Adapter
|
||||
**Extractors** (`extractors/`):
|
||||
- `jwt.rs`: Извлечение claims из `Authorization: Bearer`
|
||||
- `api_key_extractor.rs`: API key auth
|
||||
- `multitype_form_extractor.rs`: Объединяет JSON и multipart
|
||||
|
||||
**Middleware** (`middleware/`):
|
||||
- `jwt.rs`: Проверка JWT
|
||||
- `api_key_middleware.rs`: Rate limiting + API key validation
|
||||
|
||||
**Router**: Разделение на `admin.rs` и `client.rs` для безопасности
|
||||
|
||||
### `bin` — Composition Root
|
||||
Инициализация зависимостей (DI container вручную):
|
||||
```rust
|
||||
let db_pool = infrastructure::db::connect().await?;
|
||||
let trail_repo = Arc::new(TrailRepositoryImpl::new(db_pool.clone()));
|
||||
let trail_service = Arc::new(TrailService::new(trail_repo, ...));
|
||||
let router = api::router::client::create_router(trail_service, ...);
|
||||
```
|
||||
**Важно**: Никакой логики, только сборка компонентов.
|
||||
|
||||
### `migrate` — Database Schema as Code
|
||||
SeaORM миграции с timestamp-именами (`m20240910_074808`). Каждая миграция — отдельный модуль, проверяющий `sea-orm-migration::MigrationTrait`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Как начать изучение: Практический план
|
||||
|
||||
### Шаг 1: Запуск и отладка
|
||||
```bash
|
||||
# 1. Понять переменные окружения (см. .env.example или docker-compose.yml)
|
||||
DATABASE_URL=postgres://...
|
||||
|
||||
# 2. Запустить миграции
|
||||
cargo run --bin migrate
|
||||
|
||||
# 3. Запустить клиентский API
|
||||
cargo run --bin client
|
||||
|
||||
# 4. Проверить health endpoint
|
||||
curl http://localhost:3000/client/ping
|
||||
|
||||
# 5. Запустить тестовый сценарий
|
||||
hurl examples/positive/register_positive.hurl
|
||||
```
|
||||
|
||||
### Шаг 2: Изучение через тесты
|
||||
**Hurl-тесты** (`examples/positive/`) — лучшая документация:
|
||||
```hurl
|
||||
# examples/positive/user_login.hurl
|
||||
POST http://localhost:3000/client/auth/login
|
||||
{
|
||||
"username": "testuser",
|
||||
"password": "testpass123"
|
||||
}
|
||||
HTTP 200
|
||||
[Captures]
|
||||
access_token: jsonpath "$.access_token"
|
||||
```
|
||||
Это показывает **real-world usage** без изучения кода.
|
||||
|
||||
### Шаг 3: Трейсинг одной фичи
|
||||
Выберите простой endpoint и пройдитесь по нему:
|
||||
1. **GET /client/profiles/:id** → `api::endpoints::client::profiles.rs`
|
||||
2. Смотрите, как используется `ProfileService`
|
||||
3. Изучите `ProfileRepository` в `infrastructure`
|
||||
4. Проверьте `domain::client::profiles::responses::ProfileResponse`
|
||||
|
||||
### Шаг 4: Изучение доменной модели
|
||||
Прочитайте **все миграции по порядку** (`crates/migrate/src/`). Это история эволюции бизнес-требований:
|
||||
- Сначала был User и Trail
|
||||
- Потом добавили лайки, треды, статусы
|
||||
- Затем abuse system
|
||||
- Потом множественные content types
|
||||
|
||||
---
|
||||
|
||||
## 6. Паттерны и конвенции
|
||||
|
||||
### Обработка ошибок
|
||||
Все crates используют `thiserror` для domain errors и `anyhow` для верхнего уровня:
|
||||
```rust
|
||||
// domain ошибка
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TrailError {
|
||||
#[error("Trail not found")]
|
||||
NotFound,
|
||||
#[error("Permission denied")]
|
||||
Forbidden,
|
||||
}
|
||||
|
||||
// api уровень
|
||||
pub type ApiResult<T> = Result<T, (StatusCode, String)>;
|
||||
```
|
||||
|
||||
### Валидация
|
||||
**Один источник правды**: Все валидации в `domain` DTO:
|
||||
```rust
|
||||
#[derive(Validate)]
|
||||
pub struct CreateTrailRequest {
|
||||
#[validate(length(min = 3, max = 200))]
|
||||
pub title: String,
|
||||
#[validate(custom = "validate_content_type")]
|
||||
pub content_type: String,
|
||||
}
|
||||
```
|
||||
|
||||
### Фича-флаги через миграции
|
||||
Статусы, фичи и права добавляются через миграции, а не конфиги. Это делает состояние БД источником правды.
|
||||
|
||||
---
|
||||
|
||||
## 7. Где что менять: GPS для контрибьютора
|
||||
|
||||
### Добавить новое поле в Trail
|
||||
1. **Миграция**: `crates/migrate/src/m2025xxxx_add_field.rs`
|
||||
2. **Entity**: Добавить поле в `crates/entity/src/trail.rs`
|
||||
3. **Domain**: Обновить `domain::trails::NewTrail` и Response
|
||||
4. **Repository**: Изменить маппинг в `infrastructure::repositories::trail`
|
||||
5. **Form**: Обновить `infrastructure::forms::forms_trail.rs` (если поле editable)
|
||||
6. **Tests**: Добавить hurl-тест в `examples/positive/`
|
||||
|
||||
### Добавить новый endpoint
|
||||
1. **Domain**: Создать Request/Response DTO
|
||||
2. **API-core**: Создать метод в Service
|
||||
3. **API**: Добавить endpoint в `api::endpoints::client` или `admin`
|
||||
4. **Router**: Подключить в `api::router`
|
||||
5. **Hurl**: Создать тест в `examples/`
|
||||
6. **Utoipa**: Добавить `#[utoipa::path]` для Swagger-документации
|
||||
|
||||
### Исправить баг в бизнес-логике
|
||||
**Ищите в `api-core`**, а не в `api`. Например, логика перехода Draft→Public в `TrailService::update_status()`.
|
||||
|
||||
### Поменять storage backend
|
||||
Измените инициализацию в `infrastructure::services::storage_service.rs` или добавьте новый driver в `storage/drivers/`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Инструменты для вашего стека (NixOS + Helix)
|
||||
|
||||
### NixOS-specific
|
||||
- **Postgres**: `services.postgresql.enable = true;` в configuration.nix
|
||||
- **Rust toolchain**: Используйте `rust-overlay` или `fenix` для точной версии 1.92.0
|
||||
- **Дев-окружение**: Создайте `shell.nix` с зависимостями: `postgresql`, `hurl`, `docker` (for tests)
|
||||
|
||||
### Helix editor workflow
|
||||
```toml
|
||||
# .helix/languages.toml
|
||||
[language-server.rust-analyzer]
|
||||
config = {
|
||||
cargo = { features = "all" }
|
||||
}
|
||||
```
|
||||
- **Jump to definition**: `gd` перейдет от `domain` к `entity` через trait
|
||||
- **Global search**: `Space + /` ищите по `examples/` чтобы найти использование endpoint
|
||||
- **LSP go-to-impl**: Посмотреть реализацию repository
|
||||
|
||||
### Быстрый поиск по коду
|
||||
```bash
|
||||
# Найти все использования TrailService
|
||||
rg "TrailService" --type rust
|
||||
|
||||
# Найти все endpoints с /trails
|
||||
rg "trails" crates/api/src/router/
|
||||
|
||||
# Найти миграции за конкретную дату
|
||||
ls crates/migrate/src | rg "2025-02"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Риски и особенности для контрибьютора
|
||||
|
||||
### Version pinning
|
||||
- `tokio = "1,<1.48"` — **исключительно важно**. Это ограничение может быть связано с `sea-orm` или `axum` совместимостью. Не обновляйте без тестов.
|
||||
- `sea-orm = "=1.1.17"` — точная версия, возможно, из-за багов в 1.2+
|
||||
|
||||
### Async runtime
|
||||
Везде используется `tokio::main`, но `sea-orm` использует `native-tls`, а `sqlx` — `rustls`. Это может вызвать конфликты SSL-библиотек в NixOS. Следите за линковкой.
|
||||
|
||||
### OpenDAL features
|
||||
`opendal` собран без default features, только с memory и fs. Если понадобится S3, нужно расширить features в `infrastructure/Cargo.toml`.
|
||||
|
||||
### Migration dependencies
|
||||
Миграции зависят от `entity` crate. Если меняете entity, **всегда пересобирайте миграции**:
|
||||
```bash
|
||||
cargo clean -p migrate && cargo build -p migrate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Чеклист перед PR
|
||||
|
||||
- [ ] Hurl-тесты проходят: `cargo test --test hurl_test`
|
||||
- [ ] Clippy чист: `cargo clippy --all-targets --all-features`
|
||||
- [ ] Форматирование: `cargo fmt --all`
|
||||
- [ ] Миграции откатываются: `cargo run --bin migrate down`
|
||||
- [ ] OpenAPI спецификация генерируется (если меняли endpoints)
|
||||
- [ ] Логирование добавлено: `tracing::info!/`debug!` на важных шагах
|
||||
- [ ] Обработка ошибок: Domain ошибки преобразованы в правильные HTTP статусы
|
||||
|
||||
---
|
||||
|
||||
**Финальный совет**: Начните с **трех файлов**:
|
||||
1. `crates/migrate/src/lib.rs` — поймете эволюцию домена
|
||||
2. `crates/api/src/router/client.rs` — увидите все endpoint'ы
|
||||
3. `crates/api-core/src/trails.rs` — поймете бизнес-логику
|
||||
|
||||
Это даст вам mental model проекта за 30 минут. Удачи в контрибуции!
|
||||
253
00-work/issues.md
Normal file
253
00-work/issues.md
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
почему в качестве валидации email - регулярка, когда можно использовать общепринятую функцию
|
||||
|
||||
tls конфликт
|
||||
```
|
||||
sea-orm: runtime-tokio-native-tls
|
||||
sqlx: runtime-tokio-rustls
|
||||
```
|
||||
|
||||
Причина долгого исполнения тестов: компиляция сервера каждый раз.
|
||||
```rust
|
||||
Command::new("cargo")
|
||||
.args(["r", "--bin", "client"])
|
||||
```
|
||||
1. решение: предскомпилировать сервер, и переиспользовать.
|
||||
2. Распараллелить тесты (может быть сложно)
|
||||
3. **Проблема**: `DELETE FROM table;` — **медленная** операция (проверяет constraints, пишет в WAL). **TRUNCATE** — мгновенно.
|
||||
|
||||
```rust
|
||||
async fn clear_db(db: &DbConn) -> Result<(), DbErr> {
|
||||
db.execute_unprepared("
|
||||
TRUNCATE abuse, abuse_reason, admin, \"like\", mail, refreshtoken,
|
||||
thread, trail, trails_visit_history, \"user\"
|
||||
RESTART IDENTITY CASCADE;
|
||||
").await?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 1. **Токио
|
||||
|
||||
```toml
|
||||
tokio = { version = "1,<1.48", features = ["full"] }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. **SeaORM**
|
||||
|
||||
```toml
|
||||
sea-orm = { version = "=1.1.17", ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. **SQLx + SeaORM**
|
||||
|
||||
**Почему это плохо**:
|
||||
- `sea-orm` использует `sqlx` под капотом, но с другими runtime features (`native-tls` vs `rustls`)
|
||||
- У вас **два пула соединений** с БД (один от SeaORM, второй от sqlx)
|
||||
- Миграции на SeaORM, а тесты могут использовать sqlx напрямую → **schema drift** (расхождение схемы)
|
||||
|
||||
**Конкретный риск**: В `tests/Cargo.toml` вы подключаете оба. Если напишете тест, который использует `sqlx::query!` и `sea_orm::Entity::insert`, у вас будет **race condition** на уровне транзакций. PostgreSQL не увидит изменения из одного пула в другом до коммита, и ваши тесты будут flaky (нестабильными).
|
||||
|
||||
**Техдолг**: Вы платите за **complexity tax** (налог на сложность) дважды: за абстракцию ORM и за прямой доступ.
|
||||
|
||||
---
|
||||
|
||||
## 4. **Storage Service: Premature abstraction, которая убьет вас**
|
||||
|
||||
Ваша абстракция хранилища с `mirror`, `backup`, `single` стратегиями — это **over-engineering без причины**.
|
||||
|
||||
**Почему это ошибка**:
|
||||
- У вас нет SLA на durability (надежность хранения)
|
||||
- У вас нет метрик на RTO/RPO (Recovery Time/Point Objectives)
|
||||
- У вас нет **idempotency** в storage operations
|
||||
|
||||
**Конкретный баг**: В `mirror.rs` если первый write успешен, а второй — нет, у вас **silent data corruption** (тихая порча данных). У вас нет **transaction log** для отката первого write. Это **eventual consistency without reconciliation** (согласование в конечном счете без сверки).
|
||||
|
||||
**Техдолг**: Вы построили **distributed system** там, где был достаточен `std::fs::write`. Через год, когда storage заполнится, вы не поймете, почему `mirror` стратегия оставляет мусор в старых бакетах — потому что нет garbage collection.
|
||||
|
||||
---
|
||||
|
||||
## 5. **DTO Hell: Вы создаете 3-4 структуры на каждую сущность**
|
||||
|
||||
Посмотрите на `domain/src/client/profiles/requests.rs` и `responses.rs`. У вас:
|
||||
- `CreateUserRequest`
|
||||
- `UpdateUserRequest`
|
||||
- `PublicUserResponse`
|
||||
- `PrivateUserResponse`
|
||||
- `UserResponse`
|
||||
|
||||
**Это не separation of concerns, это duplication of concerns**. Каждое изменение поля `username` — это изменение в 5 файлах и 2 миграциях.
|
||||
|
||||
**Что вы потеряли**: **Single source of truth**. У вас нет `User` entity из домена, который бы гарантировал инварианты. Вместо этого вы полагаетесь на валидацию в каждом DTO отдельно.
|
||||
|
||||
**Техдолг**: **Schema spread**. Когда добавите поле `last_login_at`, вы забудете обновить один из Response DTO, и клиент получит panicking deserialization.
|
||||
|
||||
---
|
||||
|
||||
## 6. **Abuse System: Security theater (имитация безопасности)**
|
||||
|
||||
У вас есть `CreateAbuseRequest`, `abuse.rs`, `abuse_reason.rs`, но нет **rate limiting** на создание abuse-репортов. Я могу написать скрипт, который заспамит 1M репортов на один trail и **сложит вашу БД**.
|
||||
|
||||
**Что не так**:
|
||||
- Нет `abuse_reports_per_user_per_hour` констрейнта
|
||||
- Нет `abuse_score` алгоритма (например, если 10 репортов от разных юзеров — автоматический бан)
|
||||
- Нет `abuse_report_fingerprint` (я могу создавать репорты с разных аккаунтов, но с одного IP)
|
||||
|
||||
**Техдолг**: У вас есть **data model для безопасности, но нет enforcement**. Это как замок на двери без стен.
|
||||
|
||||
---
|
||||
|
||||
## 7. **Transactions: Невидимый кошмар**
|
||||
|
||||
В `repository` трейтах у вас методы типа `create_trail`, `toggle_trail_like` — каждый **своя собственная транзакция**.
|
||||
|
||||
**Почему это баг**: Если в use-case нужно создать trail, создать thread, поставить лайк — у вас **три отдельные транзакции**. Если последняя упадет, первые две останутся в БД. У вас **inconsistent state** без `ROLLBACK`.
|
||||
|
||||
**Где посмотреть**: `api-core` должен принимать `&DatabaseTransaction` и выполнять все операции в ней. Сейчас у вас **autocommit hell** — каждый `repo.insert()` — это `COMMIT`.
|
||||
|
||||
**Техдолг**: У вас нет **transactional boundaries** на уровне use-case. Это значит, ваши данные **не консистентны** в границах бизнес-операции.
|
||||
|
||||
---
|
||||
|
||||
## 8. **API Keys Middleware: Тёмная материя без тестов**
|
||||
|
||||
В `api/src/middleware/api_key_middleware.rs` (который вы не показали, но он подключен) — вероятно, нет **integration tests**.
|
||||
|
||||
**Что это значит**: Вы не знаете, работает ли rate limiting. Я могу brute-force API keys со скоростью 10k req/s, и ваша middleware может **не выдержать нагрузку**.
|
||||
|
||||
**Техдолг**: У вас **нет нагрузочных тестов** на middleware. Когда ваше приложение ляжет от DDoS, вы не поймете, виноват ли `axum`, `tower`, или ваш кастомный код.
|
||||
|
||||
---
|
||||
|
||||
## 9. **Thread Model: Race condition при append**
|
||||
|
||||
`append_trail_to_thread` — это **concurrency nightmare**.
|
||||
|
||||
**Сценарий**:
|
||||
1. User A читает thread (id=1, last_trail_id=5)
|
||||
2. User B читает thread (id=1, last_trail_id=5)
|
||||
3. Оба одновременно `append_trail_to_thread` с parent_id=5
|
||||
4. У вас два trail'а с parent_id=5, но thread не знает, кто "победил"
|
||||
|
||||
У вас нет **optimistic locking** (`version` поле) или `SELECT FOR UPDATE`. Это **lost update problem** на уровне приложения.
|
||||
|
||||
**Техдолг**: У вас **нет SERIALIZABLE isolation** для критичных операций. Ваши треды могут разветвляться в невалидные структуры.
|
||||
|
||||
---
|
||||
|
||||
## 10. **Observability: Вы слепы в production**
|
||||
|
||||
У вас есть `tracing`, но нет:
|
||||
- **Metrics** (`prometheus`, `metrics-rs`)
|
||||
- **Structured logging** (JSON logs для ELK/Loki)
|
||||
- **Distributed tracing** (OpenTelemetry для Jaeger/Tempo)
|
||||
|
||||
**Что это значит**: Когда в продакшене упадет запрос, вы увидите:
|
||||
```
|
||||
INFO: request started
|
||||
INFO: request failed
|
||||
```
|
||||
|
||||
Но не увидите: `user_id`, `trace_id`, `duration_ms`, `db_query_count`. Вы **не сможете отладить performance regression** после вашего PR.
|
||||
|
||||
**Техдолг**: У вас **нет production-grade observability**. Это значит, вы **не сможете понять, что сломали**, когда добавите ML-фичу.
|
||||
|
||||
---
|
||||
|
||||
## 11. **Release Profile: Thin LTO — это медленный production**
|
||||
|
||||
```toml
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
```
|
||||
|
||||
Для вашего уровня performance-чувствительности (RTX 3050, 24GB RAM) это **сознательный выбор скорости компиляции в ущерб runtime**.
|
||||
|
||||
**Что это значит**: Ваш бинарник будет на 15-30% медленнее, чем мог бы быть. Для "тропиночного" приложения это не критично, но если вы планируете **batch ML inference** на GPU, вы **теряете performance** из-за неинлайненных вызовов.
|
||||
|
||||
**Техдолг**: У вас **нет performance budget**. Вы не знаете, сколько RPS может выдержать ваш сервер, потому что не тестировали под нагрузкой с `lto = "fat"`.
|
||||
|
||||
---
|
||||
|
||||
## 12. **Migrations: Append-only log без checksums**
|
||||
|
||||
Ваш `Migrator` — это **poor man's Flyway**. Но нет:
|
||||
- **Checksum** каждой миграции (если кто-то изменит старую миграцию, вы не узнаете)
|
||||
- **Baseline** миграции (невозможно подключиться к существующей БД)
|
||||
- **Repair** mode (если миграция упала на production)
|
||||
|
||||
**Что это значит**: Если ваш DevOps-engineer случайно отредактирует `m20241005_112021_create_like.rs` на проде-сервере, `cargo run --bin migrate` **не заметит изменения** и накатит кривую схему.
|
||||
|
||||
**Техдолг**: У вас **нет migration integrity checks**. Это значит, схема БД — **mutable history**, а не immutable log.
|
||||
|
||||
---
|
||||
|
||||
## 13. **Dependencies: 150+ crate'ов, и вы не знаете, зачем**
|
||||
|
||||
Запустите `cargo tree | wc -l`. У вас больше 150 зависимостей для CRUD приложения.
|
||||
|
||||
**Конкретные лишние**:
|
||||
- `rust-argon2` **и** `bcrypt` — два password hashing алгоритма. Выбери один.
|
||||
- `lazy_static` **и** `once_cell` — дублирование (once_cell в std с 1.70)
|
||||
- `utoipa` **и** `utoipa-swagger-ui` — генерируете OpenAPI, но нет тестов, что спецификация валидна
|
||||
- `opendal` — вы используете только `memory` и `fs`, но это 50+ transitive dependencies
|
||||
|
||||
**Техдолг**: У вас **dependency bloat**. Каждый `cargo build` тянет мир, а security audit (cargo audit) будет плакать каждую неделю.
|
||||
|
||||
---
|
||||
|
||||
## 14. **Test Coverage: Hurl — это не enough**
|
||||
|
||||
У вас 50+ hurl файлов, но **нет unit тестов** на `api-core`. Это значит, вы тестируете **только happy path** через HTTP.
|
||||
|
||||
**Что вы не тестируете**:
|
||||
- **Error conditions**: Что если `TrailRepository::create` вернет `DbErr::ConnectionAcquire`?
|
||||
- **Edge cases**: Что если `user_id` отрицательный? (SeaORM принимает i64, но должна валидировать)
|
||||
- **Concurrency**: Что если 1000 потоков вызывают `toggle_trail_like` одновременно?
|
||||
|
||||
**Техдолг**: У вас **нет property-based testing** (proptest) и **нет fuzzing** (cargo-fuzz). Вы **не нашли race conditions и integer overflow bugs**, которые 100% есть в коде.
|
||||
|
||||
---
|
||||
|
||||
## 15. **DTO Validation: Validate на уровне HTTP, а не домена**
|
||||
|
||||
Ваша валидация в `domain` DTO — это **security theater**. Почему?
|
||||
|
||||
```rust
|
||||
#[derive(Validate)]
|
||||
pub struct CreateTrailRequest {
|
||||
#[validate(length(min = 3, max = 200))]
|
||||
pub title: String,
|
||||
}
|
||||
```
|
||||
|
||||
Эта валидация **не вызывается автоматически**. Она вызывается в **extractor**. Если кто-то создаст `Trail` напрямую через `api-core`, минуя HTTP — валидация **пропущена**.
|
||||
|
||||
**Правильно**: Валидация должна быть в `NewTrail` **domain entity**, и она должна быть **infallible constructor** (конструктор, который не позволит создать невалидный объект).
|
||||
|
||||
**Техдолг**: У вас **type safety holes**. Невалидные данные могут просочиться в БД через прямой вызов api-core.
|
||||
|
||||
---
|
||||
|
||||
## Финальная правда: Это хороший прототип, но не production system
|
||||
|
||||
Ваш проект — **MVP с правильной архитектурной формой, но без production-grade деталей**. Он выживет до первого серьезного инцидента:
|
||||
|
||||
- **До первого DDoS** — упадет, потому что нет rate limiting
|
||||
- **До первого data corruption** — потеряете данные, потому что нет транзакций
|
||||
- **До первого security breach** — скомпрометируете API keys, потому что нет audit log
|
||||
- **До первого performance regression** — не поймете, что сломали, потому что нет метрик
|
||||
|
||||
**Что делать сейчас** (порядок приоритета):
|
||||
|
||||
1. **Удалите `sqlx` из workspace** — используйте только SeaORM
|
||||
2. **Добавьте `TransactionTrait`** во все use-кейсы в `api-core`
|
||||
3. **Напишите 10 unit тестов** на `TrailService` с `MockTrailRepository` — вы поймете, где у вас **logic gaps**
|
||||
4. **Запустите `cargo audit`** и пофиксите critical уязвимости (их будет 5-10)
|
||||
5. **Добавьте один rate limiter** в `api_key_middleware` — поймете, насколько ваш design **fragile**
|
||||
|
||||
Это не "у вас всё плохо". Это "у вас всё **правильно начато, но не закончено**".
|
||||
69
00-work/onboarding.md
Normal file
69
00-work/onboarding.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
Скопировал .env из .env.example
|
||||
|
||||
выставил таким
|
||||
API_CORS="http://localhost:3000,http://localhost:8000"
|
||||
|
||||
оставил каким есть:
|
||||
DATABASE_URL="postgres://root:root@localhost/trails_db?sslmode=disable"
|
||||
POSTGRES_USER="root"
|
||||
POSTGRES_PASSWORD="root"
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_DB="trails_db"
|
||||
CLIENT_JWT_MAXAGE=60 # hours
|
||||
ADMIN_JWT_MAXAGE=60 # hours
|
||||
STORAGE_DOMAIN="https://static.thetrails.app"
|
||||
STORAGE_FOLDER="./storage"
|
||||
|
||||
API_KEY
|
||||
добавил туда чаще вывод из таких команд
|
||||
|
||||
openssl rand -base64 32
|
||||
openssl rand -hex 32
|
||||
|
||||
PGADMIN_DEFAULT_EMAIL
|
||||
PGADMIN_DEFAULT_PASSWORD
|
||||
рандомный email, password
|
||||
|
||||
storage оставил таким какой он есть
|
||||
|
||||
# Поднимаем бд
|
||||
```bash
|
||||
docker compose up -d
|
||||
|
||||
docker compose ps # проверяем логи
|
||||
|
||||
docker compose logs -f postgres # тоже логи но бд
|
||||
|
||||
docker exec -it trails_postgres psql -U root -d trails_db # тестим бд
|
||||
```
|
||||
|
||||
# Миграции:
|
||||
|
||||
```bash
|
||||
cargo run --bin migrate -- up
|
||||
|
||||
cargo run --bin migrate -- status
|
||||
```
|
||||
|
||||
# storage
|
||||
|
||||
```
|
||||
mkdir -p ./storage
|
||||
```
|
||||
|
||||
# cargo run
|
||||
|
||||
```bash
|
||||
cargo run --bin client
|
||||
|
||||
```
|
||||
|
||||
# hurl tests
|
||||
|
||||
```bash
|
||||
# поднимаем контейнер
|
||||
docker run --rm ghcr.io/orange-opensource/hurl:latest --version
|
||||
|
||||
hurl --test examples/
|
||||
```
|
||||
|
||||
3
00-work/upgrading.md
Normal file
3
00-work/upgrading.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
![[Pasted image 20251223184946.png]]
|
||||
|
||||
![[Pasted image 20251223185010.png]]
|
||||
Loading…
Add table
Add a link
Reference in a new issue