Major new feature introducing dynamic GraphQL API exposure with automatic schema generation from registered entities.
Date: January 2025
Version: 0.0.7
Impact: New feature - No breaking changes
Order, Invoice, etc.)links.yaml configurationorders, invoices, payments queries with limit/offsetorder(id), invoice(id), payment(id) queriescreateInvoiceForOrder, linkPaymentToInvoice, unlinkPaymentFromInvoicecreateLink, deleteLink for flexible link management/graphql/playground/graphql/schemaThe GraphQL exposure is built with:
Enable GraphQL feature:
[dependencies]
this-rs = { version = "0.0.7", features = ["graphql"] }
Build server with GraphQL:
let host = Arc::new(
ServerBuilder::new()
.with_link_service(InMemoryLinkService::new())
.register_module(module)?
.build_host()?
);
let graphql_router = GraphQLExposure::build_router(host)?;
List entities:
query {
orders(limit: 10) {
id
number
customerName
amount
}
}
Get with relations:
query {
order(id: "UUID") {
id
number
invoices {
id
amount
payments {
id
method
}
}
}
}
Create entity:
mutation {
createOrder(data: {
number: "ORD-001"
customerName: "John Doe"
amount: 1000.0
status: "active"
}) {
id
number
}
}
New feature introducing generic pagination and filtering for all list endpoints (entities and links), with automatic pagination by default to prevent returning all data at once.
Date: December 2024
Version: 0.0.6
Impact: Enhancement - Default pagination applied
target.status, source.name, etc.)PaginatedResponse<T> structureNo breaking changes! Pagination is now automatic:
// Before - would return ALL entities
GET /orders
// After - automatically paginated
GET /orders
Response: { "data": [...], "pagination": { "page": 1, "limit": 20, ... } }
// You can still get all data with explicit pagination
GET /orders?limit=1000
# List entities with default pagination (20 per page)
curl 'http://127.0.0.1:3000/orders'
# Custom pagination
curl 'http://127.0.0.1:3000/orders?page=2&limit=10'
# With filters
curl 'http://127.0.0.1:3000/orders?filter={"status":"pending"}'
# Links with pagination
curl 'http://127.0.0.1:3000/orders/{id}/invoices?page=1&limit=5'
# Links with filters on nested entities
curl 'http://127.0.0.1:3000/orders/{id}/invoices?filter={"target.status":"paid"}'
# Nested links (3+ levels) with pagination
curl 'http://127.0.0.1:3000/orders/{id}/invoices/{id}/payments?page=1&limit=10'
Major new feature introducing automatic validation and filtering with declarative rules defined directly in entity models.
Date: December 2024
Version: 0.0.5
Impact: New feature - No breaking changes
impl_data_entity_validated! Macro - Extended entity macro with validation supportValidated<T> Extractor - Axum extractor for automatic validationrequired, optional, positive, string_length, max_value, in_list, date_formattrim, uppercase, lowercase, round_decimalsIf youโre using impl_data_entity!, you can continue using it. However, we recommend upgrading to impl_data_entity_validated!:
// Old way (still works)
impl_data_entity!(Invoice, "invoice", ["number"], { amount: f64 });
// New way with validation
impl_data_entity_validated!(
Invoice, "invoice", ["number"], { amount: f64 },
validate: {
create: {
amount: [required positive max_value(1_000_000.0)],
},
},
filters: {
create: {
amount: [round_decimals(2)],
},
}
);
Major architectural refactoring introducing a macro-driven entity system, automatic entity creation with linking, and removal of multi-tenancy in favor of a cleaner, simpler architecture.
Date: October 2024
Version: 0.0.2
Impact: BREAKING CHANGES - Migration required
Before (v0.0.1):
struct Order {
id: Uuid,
tenant_id: Uuid, // โ Required everywhere
name: String,
amount: f64,
}
// All service methods required tenant_id
service.create(&tenant_id, order).await?;
service.get(&tenant_id, &id).await?;
After (v0.0.2):
// โ
No tenant_id!
impl_data_entity!(Order, "order", ["name"], {
amount: f64,
});
// Clean API without tenant_id
service.create(order).await?;
service.get(&id).await?;
Rationale:
3-Level Architecture:
Entity (Base)
โโโ id: Uuid
โโโ type: String
โโโ created_at: DateTime<Utc>
โโโ updated_at: DateTime<Utc>
โโโ deleted_at: Option<DateTime<Utc>> โ
Built-in soft delete
โโโ status: String
โโโบ Data (Business entities)
โ โโโ name: String
โ + custom fields...
โ
โโโบ Link (Relationships)
โโโ source_id: Uuid
โโโ target_id: Uuid
โโโ link_type: String
Benefits:
Before (v0.0.1): Manual boilerplate (50+ lines per entity)
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Order {
id: Uuid,
tenant_id: Uuid,
name: String,
amount: f64,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
// ... 10+ more lines of fields
}
impl Entity for Order {
// ... 30+ lines of trait implementation
}
impl Data for Order {
// ... 20+ lines of trait implementation
}
After (v0.0.2): Macro magic! (4 lines)
impl_data_entity!(Order, "order", ["name", "amount"], {
amount: f64,
});
// โจ Auto-generates:
// - All base Entity fields (id, type, created_at, etc.)
// - name field (from Data trait)
// - Your custom fields
// - Full trait implementations (Entity, Data)
// - Constructor: Order::new(...)
// - Utility methods: soft_delete(), touch(), set_status(), restore()
// - Serde implementations
Macros Provided:
impl_data_entity! - Complete Data entityimpl_link_entity! - Custom Link entityentity_fields! - Inject base Entity fieldsdata_fields! - Inject Entity + namelink_fields! - Inject Entity + link fieldsNew Feature: Create entities automatically when creating links!
// Implement EntityCreator on your store
#[async_trait]
impl EntityCreator for OrderStore {
async fn create_from_json(&self, entity_data: serde_json::Value) -> Result<serde_json::Value> {
let order = Order::new(
entity_data["name"].as_str().unwrap_or("").to_string(),
entity_data["status"].as_str().unwrap_or("active").to_string(),
entity_data["amount"].as_f64().unwrap_or(0.0),
);
self.add(order.clone());
Ok(serde_json::to_value(order)?)
}
}
Usage: Two ways to create links now!
POST /orders/{order_id}/invoices/{invoice_id}
Body: { "metadata": { "note": "Standard invoice" } }
POST /orders/{order_id}/invoices
Body: {
"entity": {
"name": "INV-999",
"amount": 999.99,
"status": "active"
},
"metadata": { "note": "Auto-created" }
}
# Returns BOTH entity and link!
{
"entity": { "id": "...", "name": "INV-999", ... },
"link": { "id": "...", "source_id": "...", "target_id": "...", ... }
}
Feature: Links now include full entity data automatically!
Before (v0.0.1):
{
"links": [
{
"id": "link-123",
"source_id": "order-abc",
"target_id": "invoice-xyz"
// โ Need separate query to get invoice data
}
]
}
After (v0.0.2):
{
"links": [
{
"id": "link-123",
"source_id": "order-abc",
"target_id": "invoice-xyz",
"target": {
"id": "invoice-xyz",
"type": "invoice",
"name": "INV-001",
"amount": 1500.00,
// โ
Full entity data included!
...
}
}
]
}
Smart Enrichment:
/orders/{id}/invoices) โ Only target entities included/invoices/{id}/order) โ Only source entity included/orders/{id}/invoices/{inv_id}) โ Both entities includedNew Methods:
pub trait Module: Send + Sync {
// ... existing methods ...
// NEW: Provide entity fetchers for link enrichment
fn get_entity_fetcher(&self, entity_type: &str) -> Option<Arc<dyn EntityFetcher>>;
// NEW: Provide entity creators for auto-creation
fn get_entity_creator(&self, entity_type: &str) -> Option<Arc<dyn EntityCreator>>;
}
| Metric | Before (v0.0.1) | After (v0.0.2) | Improvement |
|---|---|---|---|
| Lines per entity | ~150 | ~4 | -97% |
| Manual routing | Required | Auto-generated | 100% |
| tenant_id everywhere | Yes โ | No โ | Cleaner API |
| Link creation methods | 1 | 2 | More flexible |
| Link enrichment | Manual | Automatic | Better DX |
| Soft delete | Manual | Built-in | Standardized |
Before:
struct Order {
id: Uuid,
tenant_id: Uuid, // โ Remove
name: String,
}
service.create(&tenant_id, order).await?;
After:
impl_data_entity!(Order, "order", ["name"], {
amount: f64,
});
service.create(order).await?; // โ
No tenant_id
Before: 150+ lines of boilerplate
After: 4 lines with macro
impl_data_entity!(Order, "order", ["name", "number"], {
number: String,
amount: f64,
customer_name: Option<String>,
});
#[async_trait]
impl EntityCreator for OrderStore {
async fn create_from_json(&self, entity_data: serde_json::Value) -> Result<serde_json::Value> {
let order = Order::new(
entity_data["name"].as_str().unwrap_or("").to_string(),
entity_data["status"].as_str().unwrap_or("active").to_string(),
entity_data["number"].as_str().unwrap_or("").to_string(),
entity_data["amount"].as_f64().unwrap_or(0.0),
entity_data["customer_name"].as_str().map(String::from),
);
self.add(order.clone());
Ok(serde_json::to_value(order)?)
}
}
impl Module for BillingModule {
// ... existing methods ...
fn get_entity_fetcher(&self, entity_type: &str) -> Option<Arc<dyn EntityFetcher>> {
match entity_type {
"order" => Some(Arc::new(self.store.orders.clone())),
_ => None,
}
}
fn get_entity_creator(&self, entity_type: &str) -> Option<Arc<dyn EntityCreator>> {
match entity_type {
"order" => Some(Arc::new(self.store.orders.clone())),
_ => None,
}
}
}
Link Creation - Before:
# Only one way
POST /orders/{id}/invoices
Body: { "target_id": "{invoice_id}", "metadata": {...} }
Link Creation - After:
# Method 1: Link existing
POST /orders/{order_id}/invoices/{invoice_id}
Body: { "metadata": {...} }
# Method 2: Create new + link
POST /orders/{order_id}/invoices
Body: { "entity": {...}, "metadata": {...} }
let mut order = Order::new(...);
order.soft_delete(); // Sets deleted_at timestamp
// Order still in database, just marked as deleted
order.restore(); // Clears deleted_at
// Order is active again
let mut order = Order::new(...);
// created_at and updated_at are auto-set
order.touch(); // Updates updated_at to now
let mut order = Order::new(...);
order.set_status("completed".to_string());
// Status changed + updated_at refreshed
impl_data_entity!(Order, "order", ["name"], { amount: f64 });
// entity.entity_type() always returns "order"
// Impossible to have type mismatches!
โ
97% less boilerplate - Macros generate everything
โ
Cleaner API - No tenant_id clutter
โ
More flexibility - Two ways to create links
โ
Better DX - Auto-enriched link responses
โ
Built-in features - Soft delete, timestamps, status
โ
Faster development - Less code to write
โ
Easier maintenance - Less code to maintain
โ
Consistent patterns - Macros enforce consistency
โ
Better onboarding - Simpler concepts
โ
Performance - Efficient link enrichment
โ
Flexibility - Choose tenancy model
โ
Reliability - Type-safe, compile-time checks
โ
Scalability - Clean architecture scales well
All documentation has been updated to reflect these changes:
Planned for v0.0.3+:
| Change | Migration Effort | Impact |
|---|---|---|
| Removed tenant_id | Medium | All entities and service calls |
| New entity hierarchy | Low | Only trait implementations |
| Macro system | Low | Simplifies entity definition |
| EntityCreator trait | Low | Optional, add when needed |
| Module trait methods | Low | Just add two new methods |
This refactoring makes this-rs:
The framework is now production-ready with a clean, modern architecture! ๐๐ฆโจ
Questions or issues with migration?
Welcome to this-rs v0.0.2! ๐