make:resource, and validation all the way to the form
A list to search and page through, a form, a detail view, delete with a confirmation, and the commands and tests behind them. Freddy writes none of it by hand: rata make:resource writes it, and the code is his afterwards.
Three tiers
rata make:resource Note # commands, validation, events, tests, for your model
rata make:resource Note --view # + the Svelte screens
rata make:resource Note --generate … # + the model, migration, factory, seeder
The guide calls the result a resource: the vertical slice a desktop app needs for each kind of record. It is not a runtime you configure, but code written into your project, and it is the reference for how things are meant to be done in Elyra.
Notebooks, generated
Freddy's notebooks are a new table, so he starts with --generate. A
field is name:type[:modifier…][?][=default]:
rata make:resource Notebook --generate name:string:unique 'colour:string?'
In zsh a field with ? needs quotes. make:resource checks
that the project has the database feature, serde_json
and, for the tests, tokio, and prints the exact lines if one is
missing; it does not edit Cargo.toml itself. A parent must be
generated before a resource that references it, because the migrations run in the
order they were generated.
Notes, from the model he already has
For notes Freddy already wrote the model in chapter 5. Without
--generate, make:resource finds
#[derive(Model)] struct Note under src/ and reads it
with syn, so the commands, rules and form match its fields. The model
must derive Default, Clone, Serialize,
Deserialize and specta::Type, and have an
i64 key; rata says which are missing. Freddy adds
Default and Clone and runs it with the screens:
rata make:resource Note --view
Wire it once
The first resource prints what main.rs needs, and it is the last edit
he makes there; every later resource is picked up by the registry:
mod resources;
App::new()
.database(sqlite_url(&path))
.commands(resources::commands())
.migrations(resources::migrations())
.seeders(resources::seeders())
.allow_abilities(resources::abilities())
allow_abilities is deliberate. Each generated command is gated by an
ability, notes.view, .create, .update,
.delete, and denied until the app grants it: chapter 3's gating,
applied for him. Granting fewer makes a read-only app;
--no-abilities skips it for a prototype.
What he got
| Command | Ability | What it does |
|---|---|---|
notes_index(query) | notes.view | live: search over the text columns, sort by an allowlisted column, a page of at most 100 rows |
notes_show(id) | notes.view | live: one record, or "not found" |
notes_store(input) | notes.create | validate, insert, dispatch NoteCreated |
notes_update(id, input) | notes.update | validate with the same rules, update, dispatch NoteUpdated |
notes_destroy(id) | notes.delete | delete, a soft delete since his model has soft_deletes, and dispatch NoteDeleted |
The input type is what a form may set: no key and no timestamps, so a request cannot set them, and its fields are all optional, so a missing one comes back as a validation message instead of a decode error. The events are the hook for everything else, an audit line or a notification, through chapter 4's listeners.
On the Svelte side he got Index.svelte, Form.svelte and
Show.svelte with routes and a nav entry: a list that searches once
typing pauses, sorts by its headers, pages, and asks before deleting; a form whose
controls follow each field's type; and a detail view. Then:
cargo test resources:: # the generated tests, on throwaway SQLite files
rata migrate
rata codegen
rata dev
The generated tests run the real migrations and cover the whole slice, including validation on create and update, the sort allowlist, the paging cap, the events, that the reads are live, and that every command needs a granted ability.
Validation, all the way to the form
The rules live in RULES in the resource's commands.rs,
as Laravel-style rule strings, and any of the 44 rules work. In a command of his
own, Freddy uses the same validator directly:
Validator::new(&data)
.rules(&[
("title", "required|string|max:200"),
("notebook_id", "required|exists:notebooks,id"),
])
.validate_with(&db)
.await?;
unique and exists query the database, so they go
through the async validate_with; the synchronous
validate() panics on a database rule rather than skipping it, and an
unknown rule panics too, naming the rule and the field. A misspelt
"requried" must not quietly let invalid input through.
On the page, the failure arrives as a typed ValidationError with
Laravel's error bag, because the bridge marks the response with its kind; no
string sniffing:
import { ValidationError } from "@elyra/runtime";
try {
await api.notes_store({ title, notebook_id });
} catch (e) {
if (e instanceof ValidationError) {
errors = e.errors; // { title: ["…"], notebook_id: ["…"] }
}
}
In the generated form that is already done: a failure lands under its field.
With a lang/en.json, every label is a translation key, and passing
the app's Translator to the validator puts the messages in the user's
language. Chapter 11 sends this same error bag the other way: a 422 from Laravel
lands in the same form, per field.
The guides are Resources and Validation.
What you learned
rata make:resourcewrites commands, tests, events and, with--view, the Svelte screens;--generateadds the model and migration.- On an existing model it reads the struct, which must derive
Default,Clone,Serialize,Deserializeandspecta::Type. - Each generated command needs an ability, granted with
allow_abilities. - Rule strings validate input, database rules go through
validate_with, and aValidationErrorreaches the form per field.