Resources — rata make:resource
A resource is the vertical slice a desktop app needs for each kind of record:
a list you can search and page through, a form with validation, a detail view,
delete with a confirmation — and the Rust commands, tests, model and migration
behind them. rata make:resource writes it, typed end to end, using the
framework's own safety rails. The code is yours afterwards: it's the reference
for how you're meant to do this in Elyra, not a runtime you configure.
This guide goes from a fresh project to a working customer list. The CLI reference is in docs/cli.md; the design and its decisions are in RFC 0001.
Three tiers
rata make:resource Customer # commands, validation, events, tests — for your model
rata make:resource Customer --view # + the Svelte screens
rata make:resource Customer --generate … # + the model, migration, factory, seeder
Start with --generate for a new table; use the first two on a model you've
written yourself. With --backend /api/customers the table is a Laravel
app's, and the commands call its API; see
make:resource --backend.
1. A project with a database
rata new crm && cd crm
A resource needs the database feature, serde_json, and tokio for the
tests. make:resource checks and prints the exact lines if they're missing —
it doesn't edit Cargo.toml:
[dependencies]
elyra = { …, features = ["database"] }
serde_json = "1"
[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
2. Generate
rata make:resource Team --generate name:string:unique
rata make:resource Customer --generate \
name:string email:email:unique 'phone:string?' active:bool=true \
team_id:references:Team
A field is name:type[:modifier…][?][=default] — the table of types is in
the CLI docs. In zsh, quote a field with ?.
Generate a parent before the resources that reference it: references:Team
needs Team to exist, and the migrations run in the order you generate them.
3. Wire it once
The first resource prints what main.rs needs, and it's the last edit you
make there — every later resource is picked up by the registry:
mod resources;
App::new()
.database("sqlite://crm.db?mode=rwc")
.commands(resources::commands())
.migrations(resources::migrations())
.seeders(resources::seeders())
.allow_abilities(resources::abilities())
// …
allow_abilities is deliberate: each command is gated by an ability
(customers.view, .create, .update, .delete), denied until the app grants
it. Grant fewer to make a read-only app, or generate with --no-abilities for
a prototype.
4. Test, migrate, seed
cargo test resources:: # the generated tests, on throwaway SQLite files
rata migrate # create the tables
ELYRA_SEED=1 cargo run # 20 teams, 20 customers
The tests run the real migrations and cover the whole slice: create, list,
show, update and delete; search, the sort allowlist, paging and its cap;
validation on create and update; unique (which skips the row being saved)
and exists; the domain events; that the list and the record are live (a
write pushes the new result); and that every command needs a granted ability.
5. Run it
rata codegen # the typed api.* the views call
rata dev
Customers and Teams are in the nav. #/customers lists them,
#/customers/new creates one, #/customers/12 shows one and
#/customers/12/edit edits it. In the ⌘K palette,
typing a name finds the customer, and Enter opens it.
What you got
src/resources/mod.rs the registry — rata's, regenerated
src/resources/customer/
mod.rs what the resource contributes
model.rs #[derive(Model)] Customer + impl Factory
migration.rs CreateCustomersTable
seeder.rs CustomerSeeder
commands.rs the five commands, input/query types, events
tests.rs TestApp tests
app/src/resources/index.js the frontend registry — rata's, regenerated
app/src/resources/customers/
index.js routes, nav entry, ⌘K search
Index.svelte Form.svelte Show.svelte
Everything under a resource's folder is yours to change; the two registries
are rata's (their first line says so, and rata refuses to overwrite one that
doesn't). After deleting a resource folder by hand, rata resources:sync
rebuilds them.
The commands
| Command | Ability | What it does |
|---|---|---|
customers_index(query) |
customers.view |
live — search over the text columns, sort by an allowlisted column, a page (at most 100 rows) |
customers_show(id) |
customers.view |
live — one record, or "customer 12 not found" |
customers_store(input) |
customers.create |
validate, insert, dispatch CustomerCreated |
customers_update(id, input) |
customers.update |
validate (the same rules), update, dispatch CustomerUpdated |
customers_destroy(id) |
customers.delete |
delete — a soft delete if the model has soft_deletes — and dispatch CustomerDeleted |
CustomerInput is what a form may set: no key and no timestamps, so a request
can't set them. Its fields are all optional, so a missing one comes back as a
validation message rather than a decode error. Validation messages go through
the app's Translator when it has one.
The two reads are live commands: the views subscribe to them, and Rust re-runs them whenever a write changes what they read — in this window, another window, or a background job — so no view reloads by hand.
The events are the hook for everything else — an audit log or a notification:
App::new().listen(|e: CustomerCreated, ctx: Ctx| async move {
ctx.get::<AuditLog>().record("customer created", e.customer.id)
});
The views
They're built from @elyra/ui (RFC 0012),
so they look like the rest of the app and need no styles of their own.
- Index — a
Tableover the live list: search once typing pauses, sortable headers, paging, rows that open by click or keyboard, and delete behind a confirm. Loading, a failure and an empty list each show as such. A customer added or edited in another window appears without a reload. - Form — a
Cardwith aForm: each field's control follows its type (Input,Textarea,Checkbox), and a validation failure, from the command or from Laravel, lands under its field. A required field is marked. - Show — the record in a
Card, with edit and delete. It's live too: an edit elsewhere shows up, and a delete elsewhere turns it into "not found". - A reference (
team_id:references:Team) whose parent has its own resource is picked in aComboboxthat searches the parent'steams_indexas the user types and shows the team's name. The detail view shows the name, live fromteams_show, as a link to/teams/{id}. The name is the parent's first text field. A parent without a resource (noteams_indexandteams_showinsrc/resources/team/commands.rs) stays a number field. A user who may not view teams sees the id.
The app needs @elyra/ui (rata new adds it). An app made before 0.21
adds it to app/package.json and imports @elyra/ui/tokens.css in
main.js; make:resource --view says so and stops until then. Views
generated before keep working as they are.
With a lang/en.json, every label is a $t("customers.…") key, and their
English defaults are added to the file (one "customers" key; nothing else is
touched). Translate them in your other locales.
On a model you already have
rata make:resource Invoice # finds `#[derive(Model)] struct Invoice` under src/
rata make:resource Invoice --view # later: add the screens, keeping the commands
The model is read with syn, so the commands, rules and form match its
fields. It must derive Default, Clone, Serialize, Deserialize and
specta::Type, and have an i64 key — rata says which are missing. Without a
migration of its own, the tests create the table from the struct; keep that in
step with your real migration.
Changing a resource
The generated code is a starting point:
- Validation — edit
RULES(orrules(id)) incommands.rs; any of the 44 rules work. - Searchable / sortable columns —
SEARCHABLEandSORTABLEat the top ofcommands.rs. Anything not inSORTABLEsorts by the key. - Who can do what — the
can = "…"on each command, and what the app grants. - The screens — plain Svelte on
@elyra/ui. To change a component for the whole app, copy it withrata make:ui.
Regenerating with --force overwrites the resource's files, so do it before
you've changed them, or diff afterwards. Regenerate the way you made it: a
resource made with --generate takes the same field list again
(--generate <fields> --force). Without one, rata refuses rather than
rebuilding it from the struct and losing what the fields said. A regenerated
migration keeps its version, so a database that ran it doesn't see a new one.
Related
- Models · Migrations · Validation
- Security — abilities and the IPC surface
- Frontend runtime — the router the views use
- Testing —
TestApp - A Laravel backend —
make:resource --backend, andmake:notificationsfor Laravel's notifications