Skip to main content

Resource

Trait Resource 

Source
pub trait Resource:
    Sized
    + Send
    + Sync
    + 'static {
    type Model: Model + IntoExpr<Self::Model> + Send + Sync + Clone + 'static;
    type Form: RecordForm<Model = Self::Model>;

Show 13 methods // Provided methods fn declare() -> ResourceDef<Self> { ... } fn view_content<'a>( _cx: &'a Cx, _record: &Self::Model, ) -> Option<BoxView<'a>> { ... } fn record_label(_cx: &Cx, _record: &Self::Model) -> Option<String> { ... } fn public_url(_cx: &Cx, _record: &Self::Model) -> Option<String> { ... } fn query(_cx: &Cx) -> Query<List<Self::Model>> { ... } fn view_query(cx: &Cx) -> Query<List<Self::Model>> { ... } fn validate_record(_cx: &Cx, _form: &Self::Form) -> FieldErrors { ... } fn create_record( cx: &Cx, form: Self::Form, ex: &mut dyn Executor, ) -> impl Future<Output = Result<Self::Model>> + Send { ... } fn update_record( cx: &Cx, record: Self::Model, posted: Posted<Self::Form>, ex: &mut dyn Executor, ) -> impl Future<Output = Result<Self::Model>> + Send { ... } fn delete_record( cx: &Cx, record: &Self::Model, ex: &mut dyn Executor, ) -> impl Future<Output = Result<()>> + Send where Self: Sized { ... } fn bulk_delete_records( cx: &Cx, records: &[Self::Model], ex: &mut dyn Executor, ) -> impl Future<Output = Result<()>> + Send where Self: Sized { ... } fn after_commit( _cx: &Cx, _committed: Committed<Self::Model>, ) -> impl Future<Output = Result<()>> + Send where Self: Sized { ... } fn view_values(_cx: &Cx, _record: &Self::Model) -> HashMap<String, String> { ... }
}
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 load, display 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: Model + IntoExpr<Self::Model> + Send + Sync + Clone + 'static

The persisted model this resource administers.

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, view, 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))),
        ))
    }
}
Source

fn view_content<'a>(_cx: &'a Cx, _record: &Self::Model) -> Option<BoxView<'a>>

Renders free-form content below the detail page’s view and above its relations.

Source

fn record_label(_cx: &Cx, _record: &Self::Model) -> Option<String>

The record’s label in the detail page’s title, or None when the record has no label to show.

The detail page titles itself with this label when a resource returns Some, and with the def’s label plus the URL’s record key when it returns None, the default. The showcase’s PostResource returns the post’s title, so its heading reads the title instead of Blog Post <record key>.

A label is display text, not a key. Two records can share one (two users named Ada), so it cannot replace the primary key that keys the table’s rows and the action routes.

Source

fn public_url(_cx: &Cx, _record: &Self::Model) -> Option<String>

A public URL for one record, rendered as a “View public post”-style link on the detail and edit pages when Some.

The default declares none, so no link renders. A resource whose records have a public page overrides this with its URL — the showcase’s posts return their /blog/{id} page.

Source

fn query(_cx: &Cx) -> Query<List<Self::Model>>

Base query — the seam for a resource’s own row scoping (ADR-0002): soft deletes and row-level visibility. Every loader starts from it.

Relations are not this method’s job. The list and the export load the relations the table’s columns declare (ComputedColumn::include), and the detail page loads Self::view_query. Include a relation here only when a closure no column covers reads it on every loader’s rows: one the policy reads, or one a table’s group_by or row key reads without a column including it.

Tenancy is not this method’s job either. For a resource whose tenancy is scoped the framework ANDs the tenant filter onto whatever this returns, at every loader through scoped_query. Do not re-state it here. Code outside the framework’s loaders starts from scoped_query.

Returns the raw typed statement query, which composes generically — filter, order_by and Paginate::new work on the raw form for any M: Model — so one panel handler drives every resource’s list page.

§Keep unique constraints in step with this scope

The app-side unique pre-check probes through the same tenant-scoped query, so a #[unique] index broader than the scope is invisible: the probe misses the colliding row and the user gets a 500 instead of the inline “has already been taken”. Scope the constraint to match — #[unique(tenant_id, email)]. Not checkable at declaration time, since a query’s filters are not introspectable; upstream #117 is the fix.

Source

fn view_query(cx: &Cx) -> Query<List<Self::Model>>

The detail page’s query: Self::query plus the relations the page reads off the loaded row — in view_values or view_content — so include them here. A relation table runs its own query and needs none.

fn view_query(cx: &Cx) -> Query<List<Post>> {
    let author: Include<Post, Author> = Post::fields().author().into();
    Self::query(cx).include(author)
}

The default is Self::query unchanged. The framework ANDs the tenant scope onto it, as it does onto Self::query.

Source

fn validate_record(_cx: &Cx, _form: &Self::Form) -> FieldErrors

App-level rules on the parsed form. The errors render inline with a 200 and nothing is written; a record fn error keeps its own mapping, so a range or cross-field rule belongs here.

Each error names the key it renders under: a control’s own key, or a Repeater group’s label. A key the submitted form renders nowhere fails the submit as a declaration error instead of writing past the rule.

Source

fn create_record( cx: &Cx, form: Self::Form, ex: &mut dyn Executor, ) -> impl Future<Output = Result<Self::Model>> + 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. ex is the open transaction: run every statement through it. Return the created row; it is what Self::after_commit receives.

Source

fn update_record( cx: &Cx, record: Self::Model, posted: Posted<Self::Form>, ex: &mut dyn Executor, ) -> impl Future<Output = Result<Self::Model>> + 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.

Source

fn delete_record( cx: &Cx, record: &Self::Model, ex: &mut dyn Executor, ) -> impl Future<Output = Result<()>> + Send
where Self: Sized,

Delete the already-authorized record (#86).

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 after-commit hook receives the snapshot once the delete commits.

The default deletes the row through scoped_query, filtered to the record’s primary key. Override to delete another way, such as a soft delete.

Source

fn bulk_delete_records( cx: &Cx, records: &[Self::Model], ex: &mut dyn Executor, ) -> impl Future<Output = Result<()>> + Send
where Self: Sized,

Bulk-delete the already-authorized records: the handler fetches through the tenancy-scoped IN query inside the framework transaction and checks the policy on every row before calling this. 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 after_commit( _cx: &Cx, _committed: Committed<Self::Model>, ) -> impl Future<Output = Result<()>> + Send
where Self: Sized,

Post-commit work for a mutation this resource committed.

The place for a side effect that must not survive a rollback: an email, a webhook, an audit row, cache invalidation. Called once per successful write, after tx.commit() and before the response — running it inside a record fn would leak the effect on rollback, and the write handlers’ pool discipline forbids a second handle while the transaction is open.

Committed names the mutation and the rows it wrote: the committed row a create or update returned, the rows a delete or bulk delete removed (one call with every row). It is never called when nothing committed: a validation error, a policy denial, a failed record fn, or a failed commit all leave the hook untouched, so a rollback cannot produce the effect. A hook that returns Err is logged and ignored — the write is committed, and retries are the app’s to build; a panic surfaces as Topcoat’s panic-isolated 500.

async fn after_commit(cx: &Cx, committed: Committed<Post>) -> Result<()> {
    let mut db = db(cx); // a fresh handle is allowed here
    for post in committed.records() { notify(post).await?; }
    Ok(())
}
Source

fn view_values(_cx: &Cx, _record: &Self::Model) -> HashMap<String, String>

The record’s values for the detail page, keyed by the name each view field binds.

The detail page reads the form’s keys from RecordForm::hydrate, and this adds any key only the view shows; the form’s keys win on a collision. A resource with no form (NoForm) supplies every key its view shows here.

cx carries the database, whose schema an embedded value’s keys need (EmbeddedForm::write_form). The default is empty.

Dyn Compatibility§

This trait is not dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§