Skip to main content

Resource

Trait Resource 

Source
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§

Source

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.

Source

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§

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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(())
}
Source

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".

Implementors§