Rust Runtime Extension Points
TeaQL Rust keeps the generated Q API fluent, but moves platform-level control
into runtime assembly. Application code can describe the business query, while
UserContext owns the infrastructure boundary that decides whether the request
is safe to execute.
The most important platform hook is RequestPolicy.
Request Policy
RequestPolicy is a global runtime policy installed on UserContext. It runs
after entity-scoped RepositoryBehavior, so it is the final place to add tenant
filters, reject unsafe request shapes, cap list sizes, or enforce platform-wide
rules before a query or mutation reaches the provider.
use teaql_core::{Expr, SelectQuery};
use teaql_runtime::{RequestPolicy, RuntimeError, UserContext};
pub struct MultiTalentPolicy;
impl RequestPolicy for MultiTalentPolicy {
fn enforce_select(
&self,
ctx: &UserContext,
query: &mut SelectQuery,
) -> Result<(), RuntimeError> {
if matches!(query.entity.as_str(), "Candidate" | "TalentProfile") {
let customer_id = ctx
.get_named_resource::<u64>("customer_account_id")
.copied()
.ok_or_else(|| RuntimeError::Policy("missing customer account".to_owned()))?;
let tenant_filter = Expr::eq("customer_account_id", customer_id);
query.filter = Some(match query.filter.take() {
Some(existing) => existing.and_expr(tenant_filter),
None => tenant_filter,
});
}
if query.raw_sql.is_some() {
return Err(RuntimeError::Policy(
"raw SQL is not allowed for normal users".to_owned(),
));
}
if let Some(slice) = &mut query.slice {
if slice.limit.is_none_or(|limit| limit > 200) {
slice.limit = Some(200);
}
}
Ok(())
}
}
Register it when assembling the runtime:
let mut ctx = teaql_runtime::UserContext::new()
.with_module(multi_talent::module_with_behaviors_and_checkers())
.with_request_policy(MultiTalentPolicy);
ctx.insert_named_resource("customer_account_id", 10001_u64);
The same trait can also enforce mutation policy:
impl RequestPolicy for MultiTalentPolicy {
fn enforce_insert(
&self,
ctx: &UserContext,
command: &mut teaql_core::InsertCommand,
) -> Result<(), RuntimeError> {
if command.entity == "Candidate" {
let customer_id = ctx
.get_named_resource::<u64>("customer_account_id")
.copied()
.ok_or_else(|| RuntimeError::Policy("missing customer account".to_owned()))?;
command
.values
.insert("customer_account_id".to_owned(), customer_id.into());
}
Ok(())
}
}
Use RequestPolicy for platform-level rules such as tenant isolation,
data-residency boundaries, raw SQL restrictions, page-size caps, audit markers,
and infrastructure safety limits.
Behavior vs Policy
RepositoryBehavior is entity-scoped. RequestPolicy is platform-scoped.
Use RepositoryBehavior when the rule belongs to one entity's repository
lifecycle. Use RequestPolicy when the rule protects the whole platform or the
customer's data boundary.
Generated Q request
-> RepositoryBehavior for the entity
-> RequestPolicy on UserContext
-> Repository
-> database provider
That order lets entity code express domain behavior first. The runtime policy still gets the final decision before execution.
Entity Event Sink
EntityEventSink allows you to listen to all mutations (Create, Update, Delete, Recover) globally across your runtime context. This is highly useful for building audit logs, outbox patterns, or broadcasting integration events.
In the context initialization, you can add your custom sink to the UserContext. Here is an example of an audit logger that we implemented in our Kanban service:
use chrono::Local;
use teaql_runtime::{EntityEvent, EntityEventKind, EntityEventSink, UserContext, RuntimeError};
use teaql_core::Value;
/// Custom EntityEventSink that captures object modifications in real-time.
pub struct AuditLogSink;
impl EntityEventSink for AuditLogSink {
fn on_event(&self, ctx: &UserContext, event: &EntityEvent) -> Result<(), RuntimeError> {
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S%.3f").to_string();
let user = ctx.user_identifier().unwrap_or("anonymous").to_string();
let action_name = match event.kind {
EntityEventKind::Created => "CREATED",
EntityEventKind::Updated => "UPDATED",
EntityEventKind::Deleted => "DELETED",
EntityEventKind::Recovered => "RECOVERED",
_ => return Ok(()),
};
// Extract ID value from event record to represent entity as Type:ID
let entity_id_str = match event.values.get("id") {
Some(Value::Text(s)) => s.clone(),
Some(Value::I64(n)) => n.to_string(),
Some(Value::U64(n)) => n.to_string(),
_ => "UNKNOWN".to_owned(),
};
let entity_identity = format!("{}:{}", event.entity, entity_id_str);
let header = format!(
"[{}] - [{}] - [AUDIT] Entity [{}] was {}",
timestamp, user, entity_identity, action_name
);
println!("{}", header);
// You can also iterate through `event.changes` to track detailed field-level diffs
for change in &event.changes {
println!(" -> Field [{}]: {:?} ➔ {:?}", change.field, change.old_value, change.new_value);
}
Ok(())
}
}
To register it at application startup:
let mut ctx = teaql_runtime::UserContext::new()
.with_module(robot_kanban::module())
.with_entity_event_sink(AuditLogSink);
// Optionally attach user info so the sink can log who did it:
ctx.set_user_identifier("system-admin".to_string());
This pattern centralizes the audit trail concern, separating it entirely from business logic and repository logic.
Other Extension Points
| Extension point | What it is for | Override or implement |
|---|---|---|
| Request policy | Platform-wide data and infrastructure boundary before select/insert/update/delete/recover execution | RequestPolicy::enforce_select, enforce_insert, enforce_update, enforce_delete, enforce_recover |
| Repository behavior | Entity-scoped repository lifecycle customization | RepositoryBehavior::before_select, before_insert, before_update, before_delete, before_recover, relation_loads |
| Checkers | Validation and record fixing with translated validation messages | Checker::check_and_fix or TypedChecker::check_and_fix_typed |
| Event sink | Audit, outbox, or integration events after mutations | EntityEventSink::on_event |
| ID generation | Custom internal ID allocation | InternalIdGenerator::generate_id |
| Schema provider | Provider-owned schema bootstrap | SchemaProvider::ensure_schema |
| Query executor | Database or storage execution implementation | Provider executor traits used by the repository layer |
| Runtime module | Generated metadata, repositories, behaviors, and checkers registration | RuntimeModule, generated module(), module_with_behaviors(), module_with_behaviors_and_checkers() |
| Context resources | Request-scoped infrastructure, tenant, operator, trace, and service objects | UserContext::insert_resource, insert_named_resource, put_local |
Design Rule
Keep generated query code readable:
let candidates = Q::candidates().select_name()
.select_email()
.with_skills_contain("Rust")
.page(0, 20)
.comment("Query candidates").purpose("Load data")
.execute_for_list(&ctx)
.await?;
Then use runtime policy to make the request safe to execute. That keeps business intent visible while letting platform engineers enforce security, observability, and operational limits in one place.