Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Huskarl AI colleagues on your own Mac Starf An activity monitor for Apple silicon that never invents a number etrans An SSH and SFTP client for macOS Elyra VM Virtual machines for macOS, Linux and Windows on your Mac Litr A small, native web browser for macOS Notr A notebook for macOS e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Refr Local-first PDF workspace for macOS Elyra Workspace A desktop workspace for coding agents Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Release notes
Changelog
Elyra
Resources — `rata make:resource`

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 Table over 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 Card with a Form: 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 a Combobox that searches the parent's teams_index as the user types and shows the team's name. The detail view shows the name, live from teams_show, as a link to /teams/{id}. The name is the parent's first text field. A parent without a resource (no teams_index and teams_show in src/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 (or rules(id)) in commands.rs; any of the 44 rules work.
  • Searchable / sortable columns — SEARCHABLE and SORTABLE at the top of commands.rs. Anything not in SORTABLE sorts 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 with rata 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