pub trait Resource:
Sized
+ Send
+ Sync
+ 'static {
type Model: ModelColumns + IntoExpr<Self::Model> + Send + Sync + Clone + 'static;
type Form: RecordForm<Model = Self::Model>;
// Provided methods
fn declare() -> ResourceDef<Self> { ... }
fn validate_record(
_cx: &Cx,
_form: &Self::Form,
) -> FieldErrors<<Self::Form as RecordForm>::Field> { ... }
fn create_record(
cx: &Cx,
form: Self::Form,
ex: &mut dyn Executor,
) -> impl Future<Output = Result<Self::Model, WriteError<<Self::Form as RecordForm>::Field>>> + Send { ... }
fn update_record(
cx: &Cx,
record: Self::Model,
posted: Posted<Self::Form>,
ex: &mut dyn Executor,
) -> impl Future<Output = Result<Self::Model, WriteError<<Self::Form as RecordForm>::Field>>> + Send { ... }
fn delete_record(
cx: &Cx,
record: &Self::Model,
ex: &mut dyn Executor,
) -> impl Future<Output = Result<(), WriteError>> + Send { ... }
fn restore_record(
cx: &Cx,
record: &Self::Model,
ex: &mut dyn Executor,
) -> impl Future<Output = Result<(), WriteError>> + Send { ... }
fn force_delete_record(
cx: &Cx,
record: &Self::Model,
ex: &mut dyn Executor,
) -> impl Future<Output = Result<(), WriteError>> + Send { ... }
fn bulk_delete_records(
cx: &Cx,
records: &[Self::Model],
ex: &mut dyn Executor,
) -> impl Future<Output = Result<(), WriteError>> + Send { ... }
fn before_commit(
_cx: &Cx,
_change: &Change<Self::Model>,
_ex: &mut dyn Executor,
) -> impl Future<Output = Result<()>> + Send { ... }
fn after_commit(
_cx: &Cx,
_change: Change<Self::Model>,
) -> impl Future<Output = Result<()>> + Send { ... }
}Expand description
Maps one Toasty Model to its admin UI.
§Contract
Requires Model and Form; every other item has a default.
declare returns what the resource declares, as one ResourceDef value;
the methods scope, validate and write records for a request.
The panel builds the def once when it mounts, binding the paths it names to the database schema, and serves the result to every request. Record fns run inside the handler transaction and fail without partial writes.
Required Associated Types§
Sourcetype Model: ModelColumns + IntoExpr<Self::Model> + Send + Sync + Clone + 'static
type Model: ModelColumns + IntoExpr<Self::Model> + Send + Sync + Clone + 'static
The persisted model this resource administers. Its ModelColumns
give the table and detail page the resource declares none of.
Sourcetype Form: RecordForm<Model = Self::Model>
type Form: RecordForm<Model = Self::Model>
The typed value the create and edit forms parse into.
A resource with create and edit pages names its
#[derive(RecordForm)] struct; a list-only resource
names NoForm<Self::Model>, and
Panel::resource registers no form route for
it (RecordForm::HAS_FORM).
Provided Methods§
Sourcefn declare() -> ResourceDef<Self>
fn declare() -> ResourceDef<Self>
What the resource declares: names, navigation, policy, tenancy, table, form, detail page,
relations and actions. Defaults to ResourceDef::new, whose policy denies all.
impl Resource for PostResource {
type Model = Post;
type Form = PostForm;
fn declare() -> ResourceDef<Self> {
let c = PostForm::controls();
ResourceDef::new().policy(ReadOnly).form(Schema::new(
Section::new("Content").schema((c.title, c.body.multiline(6))),
))
}
}The panel builds the def once when it mounts and serves that copy to every request, so
declare takes no user, tenant or query string: a request-dependent decision belongs to
the policy or to a trait method, which receives cx.
Sourcefn validate_record(
_cx: &Cx,
_form: &Self::Form,
) -> FieldErrors<<Self::Form as RecordForm>::Field>
fn validate_record( _cx: &Cx, _form: &Self::Form, ) -> FieldErrors<<Self::Form as RecordForm>::Field>
App-level rules on the parsed form. The errors render inline with a
200 and nothing is written, so a range or cross-field rule belongs here. A rule that
reads the database belongs in create_record or
update_record, which refuse through the transaction they write
in.
Each error names a field of the record form (UserFormField::Age) and renders under its
control, or under an embedded value’s first control.
Sourcefn create_record(
cx: &Cx,
form: Self::Form,
ex: &mut dyn Executor,
) -> impl Future<Output = Result<Self::Model, WriteError<<Self::Form as RecordForm>::Field>>> + Send
fn create_record( cx: &Cx, form: Self::Form, ex: &mut dyn Executor, ) -> impl Future<Output = Result<Self::Model, WriteError<<Self::Form as RecordForm>::Field>>> + Send
Create a record from the parsed form, inside the handler’s transaction.
Defaults to the derived write, write_create; override to check
something inside the transaction, then delegate. An override on a
Tenancy::column resource stamps the tenant
only by delegating to write_create: the handler re-reads the returned row through the
request’s tenant scope before it commits, and a row outside it fails the write closed.
ex is the open transaction: run every statement through it. Return the created row; the
hooks receive it as Change::Create.
A rule that reads the database refuses the submission with WriteError::Refused: the
transaction rolls back and the form renders again with the errors under their fields. A
reason that names no field declines it with WriteError::declined, which the form
renders above its fields.
Sourcefn update_record(
cx: &Cx,
record: Self::Model,
posted: Posted<Self::Form>,
ex: &mut dyn Executor,
) -> impl Future<Output = Result<Self::Model, WriteError<<Self::Form as RecordForm>::Field>>> + Send
fn update_record( cx: &Cx, record: Self::Model, posted: Posted<Self::Form>, ex: &mut dyn Executor, ) -> impl Future<Output = Result<Self::Model, WriteError<<Self::Form as RecordForm>::Field>>> + Send
Update the already-authorized record from the posted form, inside the
handler’s transaction.
Defaults to the derived write, write_update. record is the
snapshot the handler loaded and policy-checked inside the transaction:
use it, never re-query. Return the row as it now stands: a row the request’s tenant scope
no longer holds fails the write closed, as in create_record.
Refuses or declines the submission as create_record does.
Sourcefn delete_record(
cx: &Cx,
record: &Self::Model,
ex: &mut dyn Executor,
) -> impl Future<Output = Result<(), WriteError>> + Send
fn delete_record( cx: &Cx, record: &Self::Model, ex: &mut dyn Executor, ) -> impl Future<Output = Result<(), WriteError>> + Send
Delete the already-authorized record.
The handler loads record through the tenancy-scoped query inside the
framework transaction and checks the policy on that snapshot, then
calls this with the same transaction as ex. The hooks receive the
snapshot as Change::Delete.
The default is write_delete: it deletes the row through scoped_query, filtered to
the record’s primary key, or stamps its soft_delete column
with the time when the resource declares one. Override to delete another way, or to refuse
with WriteError::declined and delegate the rest to write_delete: the transaction
rolls back and the message flashes, as it does for the restore, the force delete and the
bulk delete.
Sourcefn restore_record(
cx: &Cx,
record: &Self::Model,
ex: &mut dyn Executor,
) -> impl Future<Output = Result<(), WriteError>> + Send
fn restore_record( cx: &Cx, record: &Self::Model, ex: &mut dyn Executor, ) -> impl Future<Output = Result<(), WriteError>> + Send
Restore the already-authorized trashed record of a resource that
soft-deletes, inside the handler’s transaction.
The handler loads record through scoped_query_in with Trashed::Only and checks
Ability::Restore on it. The default, write_restore, empties
the soft-delete column.
Sourcefn force_delete_record(
cx: &Cx,
record: &Self::Model,
ex: &mut dyn Executor,
) -> impl Future<Output = Result<(), WriteError>> + Send
fn force_delete_record( cx: &Cx, record: &Self::Model, ex: &mut dyn Executor, ) -> impl Future<Output = Result<(), WriteError>> + Send
Delete the already-authorized trashed record for good, inside the handler’s
transaction.
The handler loads record through scoped_query_in with Trashed::Only and checks
Ability::ForceDelete on it. The default,
write_force_delete, removes the row.
Sourcefn bulk_delete_records(
cx: &Cx,
records: &[Self::Model],
ex: &mut dyn Executor,
) -> impl Future<Output = Result<(), WriteError>> + Send
fn bulk_delete_records( cx: &Cx, records: &[Self::Model], ex: &mut dyn Executor, ) -> impl Future<Output = Result<(), WriteError>> + Send
Bulk-delete the already-authorized records: the handler
fetches through the tenancy-scoped IN query inside the framework
transaction, checks View on every row, and passes the rows
Ability::Delete allows, skipping the rest
as a bulk custom action does. The default deletes each record through
Self::delete_record in order, through the same ex — any error rolls the whole
batch back, so mid-loop failures delete zero rows. An override of delete_record,
such as a soft delete, therefore covers bulk delete too. Override this
for a single-statement batch.
Sourcefn before_commit(
_cx: &Cx,
_change: &Change<Self::Model>,
_ex: &mut dyn Executor,
) -> impl Future<Output = Result<()>> + Send
fn before_commit( _cx: &Cx, _change: &Change<Self::Model>, _ex: &mut dyn Executor, ) -> impl Future<Output = Result<()>> + Send
Work inside the write’s transaction, after the record fn or action wrote: an audit row,
a counter, a row in another table that must commit with the write or not at all.
Work every resource of a panel does, such as an audit trail, belongs in a
CommitHook, which the panel runs right after this one.
Called once per write, with the Change it made, through ex, the open transaction:
run every statement through it, and notify through
Notifications::send_in, which stores the
notification only if the write commits. A second connection, through
db or Notifications::send,
would write outside the transaction, and on a pool of one wait forever for the connection
the transaction holds; either refuses instead, and the write rolls back.
Every write this resource’s panel routes make comes here: a create, once per row of a CSV
import, an update, a delete or bulk delete, a restore or force delete, a custom or header
action, and a many-to-many attach or detach. The profile and tenant-settings pages save
their user and tenant rows themselves and do not. A validation error, a policy denial or
a failed or refused record fn returns before it. An Err rolls the whole write back and
fails the request with the write’s failure toast.
async fn before_commit(
_cx: &Cx,
change: &Change<Post>,
ex: &mut dyn toasty::Executor,
) -> Result<()> {
let kind = match change {
Change::Create(_) => "create",
Change::Update { .. } => "update",
Change::Delete(_) => "delete",
Change::Restore(_) => "restore",
Change::ForceDelete(_) => "force delete",
Change::Action { name, .. } | Change::HeaderAction { name } => name,
Change::Attach { .. } => "attach",
Change::Detach { .. } => "detach",
};
for post in change.records() {
toasty::create!(Audit {
post_id: post.id,
change: kind.to_string()
})
.exec(&mut *ex)
.await?;
}
Ok(())
}Sourcefn after_commit(
_cx: &Cx,
_change: Change<Self::Model>,
) -> impl Future<Output = Result<()>> + Send
fn after_commit( _cx: &Cx, _change: Change<Self::Model>, ) -> impl Future<Output = Result<()>> + Send
Work after the write commits, outside the database: an email, a webhook, cache invalidation.
Called once per committed write, after tx.commit() and before the response, with the
Change Self::before_commit received. It is never called when nothing committed, so
a rollback cannot produce the effect. Nothing guarantees it runs either: an error it
returns is logged and ignored, since the write is committed, and a crash between the
commit and the hook skips it. A row that must exist whenever the write does belongs in
Self::before_commit; retries are the app’s to build. A panic surfaces as Topcoat’s
panic-isolated 500.
async fn after_commit(cx: &Cx, change: Change<Post>) -> Result<()> {
if let Change::Create(post) = &change {
notify(cx, post).await?;
}
Ok(())
}Dyn Compatibility§
This trait is not dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".