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§
Sourcetype Model: Model + IntoExpr<Self::Model> + Send + Sync + Clone + 'static
type Model: Model + IntoExpr<Self::Model> + Send + Sync + Clone + 'static
The persisted model this resource administers.
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, 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))),
))
}
}Sourcefn view_content<'a>(_cx: &'a Cx, _record: &Self::Model) -> Option<BoxView<'a>>
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.
Sourcefn record_label(_cx: &Cx, _record: &Self::Model) -> Option<String>
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.
Sourcefn public_url(_cx: &Cx, _record: &Self::Model) -> Option<String>
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.
Sourcefn query(_cx: &Cx) -> Query<List<Self::Model>>
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.
Sourcefn view_query(cx: &Cx) -> Query<List<Self::Model>>
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.
Sourcefn validate_record(_cx: &Cx, _form: &Self::Form) -> FieldErrors
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.
Sourcefn create_record(
cx: &Cx,
form: Self::Form,
ex: &mut dyn Executor,
) -> impl Future<Output = Result<Self::Model>> + Send
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.
Sourcefn update_record(
cx: &Cx,
record: Self::Model,
posted: Posted<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
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.
Sourcefn delete_record(
cx: &Cx,
record: &Self::Model,
ex: &mut dyn Executor,
) -> impl Future<Output = Result<()>> + Sendwhere
Self: Sized,
fn delete_record(
cx: &Cx,
record: &Self::Model,
ex: &mut dyn Executor,
) -> impl Future<Output = Result<()>> + Sendwhere
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.
Sourcefn bulk_delete_records(
cx: &Cx,
records: &[Self::Model],
ex: &mut dyn Executor,
) -> impl Future<Output = Result<()>> + Sendwhere
Self: Sized,
fn bulk_delete_records(
cx: &Cx,
records: &[Self::Model],
ex: &mut dyn Executor,
) -> impl Future<Output = Result<()>> + Sendwhere
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.
Sourcefn after_commit(
_cx: &Cx,
_committed: Committed<Self::Model>,
) -> impl Future<Output = Result<()>> + Sendwhere
Self: Sized,
fn after_commit(
_cx: &Cx,
_committed: Committed<Self::Model>,
) -> impl Future<Output = Result<()>> + Sendwhere
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(())
}Sourcefn view_values(_cx: &Cx, _record: &Self::Model) -> HashMap<String, String>
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".