#[derive(Outbox)]
{
// Attributes available to this derive:
#[outbox]
#[field]
#[sqlx]
}
Expand description
Describes a service’s outbox table with a struct: implements OutboxTable, the description
a record by hand writes, and HeaderRow for a headers field.
The struct is an ordinary sqlx struct: #[outbox(..)] names the table, sqlx’s own attributes
name the columns, and #[field(..)] marks the columns the outbox reads. The service writes the
record of a published message itself, in outbox::Publish; the registry runs the other
events on every OutboxDatabase, from statements it builds from the description when the
record type is registered. The derive checks the description against each built-in dialect
while the service compiles.
§Examples
use ruststream::OutgoingMessage;
use ruststream_sqlx::{Outbox, outbox};
use sqlx::types::Json;
use sqlx::{PgConnection, Postgres};
use std::collections::BTreeMap;
// app.outbox: id BIGSERIAL PRIMARY KEY, name TEXT, payload BYTEA, headers JSONB,
// processed_at TIMESTAMPTZ
#[derive(Outbox, sqlx::FromRow)]
#[outbox(table = "outbox", schema = "app")]
pub struct OrderEvent {
#[field(id)]
id: i64,
#[field(name)]
name: String,
#[field(payload)]
payload: Vec<u8>,
#[field(headers)]
headers: Option<Json<BTreeMap<String, String>>>,
#[field(processed_at)]
processed_at: Option<chrono::DateTime<chrono::Utc>>,
}
impl outbox::Publish<Postgres> for OrderEvent {
async fn publish(
conn: &mut PgConnection,
msg: &OutgoingMessage<'_>,
) -> Result<i64, sqlx::Error> {
let headers: BTreeMap<String, String> = msg
.headers()
.iter()
.map(|(name, value)| (name.to_owned(), String::from_utf8_lossy(value).into_owned()))
.collect();
sqlx::query_scalar(
"INSERT INTO app.outbox (name, payload, headers) VALUES ($1, $2, $3) RETURNING id",
)
.bind(msg.name())
.bind(msg.payload())
.bind(Json(headers))
.fetch_one(conn)
.await
}
}§The table
#[outbox(table = "..")] names the table and is required; schema = ".." places it in a
schema. Each names one thing and holds no dot.
§Roles
#[field(..)] gives a field one role:
id, required: the record’s identity, which a tracked message carries inOUTBOX_ID_HEADERthrough itsDisplayandFromStr, and which the default events bind as its own type.name, required: the name the record was published under, read throughAsRef<str>.payload, required: the published bytes, read throughAsRef<[u8]>.headers: the published headers, aHeaderColumntype.processed_at: the mark of a processed record, written from the database’s clock, so any time type fits. Without it a processed record is deleted.
Every other field is a plain column sqlx reads. Column names come from sqlx’s attributes:
rename, rename_all, a raw identifier without r#, and skip for a field without a column;
a flatten field makes the statements select *.
§Events
The default Fetch reads the record while it is unprocessed, Ack and Discard mark it
processed, Retry leaves it as it is and runs no statement, and Recover reads the
unprocessed records of one name. #[outbox(custom(fetch, ack, retry, discard, recover))]
lists the events the service implements itself instead; publish has no default and is never
listed.
§Compile errors
A struct the outbox cannot read does not compile, and the error points at the struct, the
field or the word that causes it: no id, name or payload, a role played twice, a column
named twice, a role on a field without a column, an unknown role or event, a dot in table or
schema, an id read through #[sqlx(json)].
Implemented in ruststream-sqlx-macros and used through ruststream-sqlx.