- Stack: Bun, Hono, Drizzle ORM, Zod, Handlebars, Pino - Models: ticket, queue, transaction, scrip, template, custom_field, user, lifecycle - Scrip engine: prepare/commit two-phase dispatch, template rendering, mock actions - Lifecycle validator: state machine transition validation with wildcard support - Routes: health, tickets (full CRUD + preview + transactions), queues, scrips, custom-fields, lifecycles - Middleware: Pino logging, error handler - Database: Drizzle ORM schema + initial migration (10 tables) - Type-check: passes (tsc --noEmit, zero errors)
9.1 KiB
9.1 KiB
Scaffold the Tessera Rust project workspace at /home/gjermund/projects/tessera.
What to Create
1. Workspace Cargo.toml
At /home/gjermund/projects/tessera/Cargo.toml:
- [workspace] with members: crates/tessera-core, crates/tessera-api
- [workspace.dependencies] with versions for:
- axum 0.8.x
- tokio 1.x (full features)
- serde 1.x (derive feature)
- serde_json 1.x
- sqlx 0.8.x (runtime-tokio, tls-rustls, postgres features)
- uuid 1.x (v4, serde features)
- chrono 0.4.x (serde feature)
- tracing 0.1.x, tracing-subscriber 0.3.x
- tower 0.5.x, tower-http 0.6.x (cors feature)
- tonic 0.12.x
- prost 0.13.x
- anyhow 1.x
- thiserror 2.x
- lettre 0.11.x (rustls-tls feature)
- tera 1.x
- figment 0.10.x (toml, env features)
- dotenvy 0.15.x
- clap 4.x (derive feature)
2. tessera-core crate
At /home/gjermund/projects/tessera/crates/tessera-core/Cargo.toml:
- name = "tessera-core"
- Dependencies: serde, serde_json, sqlx, uuid, chrono, anyhow, thiserror, tracing, tera, lettre
- [lib] crate-type = ["lib"]
At /home/gjermund/projects/tessera/crates/tessera-core/src/lib.rs:
- Module declarations: pub mod models; pub mod scrip; pub mod lifecycle; pub mod query; pub mod db;
- Re-export key types
Create these source files (with proper module structure):
crates/tessera-core/src/models/mod.rs
- Module declarations: mod ticket; mod queue; mod transaction; mod scrip; mod template; mod custom_field; mod user;
- Re-exports
crates/tessera-core/src/models/ticket.rs
- Ticket struct with fields: id (Uuid), subject (String), queue_id (Uuid), status (String), owner_id (Option), creator_id (Uuid), created_at (DateTime), updated_at (DateTime), started_at (Option<DateTime>), resolved_at (Option<DateTime>)
crates/tessera-core/src/models/queue.rs
- Queue struct: id (Uuid), name (String), description (Option), lifecycle_id (Option), created_at (DateTime)
crates/tessera-core/src/models/transaction.rs
- Transaction struct: id (Uuid), ticket_id (Uuid), transaction_type (String), field (Option), old_value (Option), new_value (Option), data (Option<serde_json::Value>), creator_id (Uuid), created_at (DateTime)
crates/tessera-core/src/models/scrip.rs
- Scrip struct: id (Uuid), queue_id (Option), name (String), description (Option), condition_type (String), condition_config (serde_json::Value), action_type (String), action_config (serde_json::Value), template_id (Option), stage (String), sort_order (i32), disabled (bool), created_at (DateTime)
- ScripStage enum: TransactionCreate, TransactionBatch
crates/tessera-core/src/models/template.rs
- Template struct: id (Uuid), name (String), queue_id (Option), subject_template (String), body_template (String), created_at (DateTime)
crates/tessera-core/src/models/custom_field.rs
- CustomField struct: id (Uuid), name (String), field_type (String), values (Option<serde_json::Value>), max_values (i32), pattern (Option), created_at (DateTime)
- QueueCustomField struct: id (Uuid), queue_id (Uuid), custom_field_id (Uuid), sort_order (i32)
- CustomFieldValue struct: id (Uuid), custom_field_id (Uuid), ticket_id (Uuid), value (String), created_at (DateTime)
crates/tessera-core/src/models/user.rs
- User struct: id (Uuid), username (String), email (Option), created_at (DateTime)
crates/tessera-core/src/scrip/mod.rs
- Module declarations: mod engine; mod conditions; mod actions; mod templates;
- Re-export ScripEngine, PreparedScrip
crates/tessera-core/src/scrip/engine.rs
- ScripEngine struct with methods:
- new(pool: PgPool)
- async fn prepare(&self, ticket_id: Uuid, transactions: &[Transaction]) -> Result<Vec>
- Load matching scrips from DB (global + queue-specific, filtered by transaction types matching condition_type, sorted by sort_order)
- For each: evaluate condition → if matches, build PreparedScrip with template substitution
- async fn commit(&self, prepared: Vec) -> Result<Vec>
- Execute each action, record results
- PreparedScrip struct: scrip_id, scrip_name, action_type, action_payload (serde_json::Value), dry_run bool
- ScripResult struct: scrip_id, success bool, message String
crates/tessera-core/src/scrip/conditions.rs
- ConditionEvaluator trait with fn evaluate(&self, ticket: &Ticket, transactions: &[Transaction]) -> bool
- OnCreate condition (true if any transaction is type "Create")
- OnStatusChange condition (true if any transaction is type "StatusChange")
- OnResolve condition (true if any transaction changes status to a "resolved" lifecycle state)
crates/tessera-core/src/scrip/actions.rs
- ActionExecutor trait with async fn execute(&self, payload: &serde_json::Value) -> Result
- SendEmail action (placeholder — logs the email it would send)
- Webhook action (placeholder — logs the webhook it would fire)
- SetCustomField action (placeholder — logs the CF it would set)
crates/tessera-core/src/scrip/templates.rs
- TemplateRenderer struct that uses Tera
- async fn render(&self, template: &Template, context: &serde_json::Value) -> Result<(String, String)> — returns (subject, body)
crates/tessera-core/src/lifecycle/mod.rs
- Lifecycle struct: id (Uuid), name (String), definition (serde_json::Value)
- LifecycleValidator struct with methods:
- fn validate_transition(&self, lifecycle_def: &serde_json::Value, from: &str, to: &str) -> Result<()>
- fn is_valid_status(&self, lifecycle_def: &serde_json::Value, status: &str) -> bool
crates/tessera-core/src/query/mod.rs
- Placeholder module with comment: "TicketSQL query builder — post-MVP"
crates/tessera-core/src/db/mod.rs
- pub mod migrations;
- Pub async fn run_migrations(pool: &PgPool) -> Result<()>
crates/tessera-core/src/db/migrations.rs
- Function signatures for migration runner (placeholder — actual migrations in sql files)
3. tessera-api crate
At /home/gjermund/projects/tessera/crates/tessera-api/Cargo.toml:
- name = "tessera-api"
- Dependencies: tessera-core (path), axum, tokio, serde, serde_json, uuid, anyhow, tracing, tracing-subscriber, tower-http, clap, figment, dotenvy
- bin name = "tessera-api"
At /home/gjermund/projects/tessera/crates/tessera-api/src/main.rs:
- Clap CLI with subcommands: serve, migrate
- serve: start axum HTTP server
- migrate: run database migrations
- tracing_subscriber init with JSON formatting
- Load config from TESSERA_CONFIG env or tessera.toml
Create source files:
crates/tessera-api/src/routes/mod.rs
- Module declarations: mod health; mod tickets; mod queues; mod scrips; mod custom_fields; mod lifecycles;
crates/tessera-api/src/routes/health.rs
- GET /health → 200 with {"status": "ok", "version": "0.1.0"}
crates/tessera-api/src/routes/tickets.rs
- Router function returning axum Router with placeholder handlers:
- GET /api/tickets
- POST /api/tickets
- GET /api/tickets/:id
- PATCH /api/tickets/:id
- POST /api/tickets/:id/preview
- GET /api/tickets/:id/transactions
- POST /api/tickets/:id/comment
- Each handler returns 501 NotImplemented with a descriptive message
crates/tessera-api/src/routes/queues.rs
- GET /api/queues → 501
- POST /api/queues → 501
crates/tessera-api/src/routes/scrips.rs
- GET /api/scrips → 501
- POST /api/scrips → 501
- GET /api/scrips/:id → 501
- PATCH /api/scrips/:id → 501
crates/tessera-api/src/routes/custom_fields.rs
- GET /api/custom-fields → 501
- POST /api/custom-fields → 501
crates/tessera-api/src/routes/lifecycles.rs
- GET /api/lifecycles → 501
- POST /api/lifecycles → 501
crates/tessera-api/src/config.rs
- AppConfig struct: database_url (String), server_host (String), server_port (u16)
- impl from figment
4. SQL Migrations
At /home/gjermund/projects/tessera/migrations/0001_initial_schema.sql:
- All CREATE TABLE statements from the architecture document (tickets, queues, lifecycles, transactions, scrips, templates, custom_fields, queue_custom_fields, custom_field_values, users)
- CREATE INDEX statements on: tickets(queue_id), tickets(status), transactions(ticket_id), transactions(created_at), scrips(queue_id), custom_field_values(ticket_id), custom_field_values(custom_field_id)
5. Configuration
At /home/gjermund/projects/tessera/tessera.example.toml:
[database]
url = "postgres://tessera:tessera@localhost:5432/tessera"
[server]
host = "127.0.0.1"
port = 8080
6. .gitignore
Standard Rust .gitignore: target/, .env, *.log
Rules
- Use
cargo initwhere appropriate, or create files manually - All code must compile: run
cargo check --workspaceafter creating everything - Fix any compilation errors
- Use
cargo fmtandcargo clippyon the workspace - Do NOT create a separate Cargo.lock — it will be generated
- The workspace Cargo.toml must NOT have a [package] section
Verification
After creating all files, run:
cargo check --workspace— must succeedcargo fmt --check— must passcargo clippy --workspace— should pass or have only minor warnings
If any verification fails, fix the issues and re-run until clean.