Skip to content

Generators (adonia:* ace commands)

Implements the TECH_SPEC §3 command table. The commands ship in @adonia/devtools and are registered through adonisrc.ts:

import { defineConfig } from '@adonisjs/core/app'

export default defineConfig({
  commands: [() => import('@adonia/devtools/commands')],
})

node ace add @adonia/core adds that entry for you.

Command Writes
adonia:resource <Model> app/adonia/resources/<model>_resource.ts
adonia:action <name> app/adonia/actions/<name>_action.ts
adonia:widget <name> app/adonia/widgets/<name>_widget.ts
adonia:field <name> app/adonia/fields/<name>_field.ts + inertia/adonia/fields/<name>_field.tsx
adonia:page <name> app/adonia/pages/<name>_page.ts + inertia/pages/adonia/<name>.tsx
adonia:eject <page> replaces inertia/pages/adonia/<page>.tsx
adonia:doctor nothing — see doctor.md

Every generator refuses to overwrite an existing file without --force.


adonia:resource <Model>

Reads the model’s columns and writes a resource whose schema mirrors the table.

node ace adonia:resource Post
node ace adonia:resource Post --fields=title,slug,body --no-table
node ace adonia:resource Post --panel=admin

Where the facts come from

Two sources are merged, plus one probe:

Fact Source
attributes, column names, primary key, serializeAs: null Model.$columnsDefinitions
date vs datetime, createdAt/updatedAt markers the column’s Lucid meta
storage type, NOT NULL, declared length columnsInfo() (knex)
enum members a per-dialect probe (below)

Enum members are not part of columnInfo in any dialect, so they are recovered separately: from the stored CREATE TABLE text on SQLite, from information_schema.COLUMNS.COLUMN_TYPE on MySQL, from pg_get_constraintdef plus pg_enum on PostgreSQL, and from sys.check_constraints on MSSQL. A dialect without a probe, or a probe that fails, leaves the column as plain text and prints why.

Type mapping

Column type Field Column (table())
varchar, char, uuid F.text C.text
text, longtext, citext F.textarea C.text
integer, bigint, smallint F.number C.number
decimal, numeric, float, real F.number C.number
boolean, bit F.toggle C.boolean
date F.date C.date
datetime, timestamp F.datetime C.date
time F.time C.text
enum F.select().options({...}) C.badge
json, jsonb F.json when core exposes it, else F.textarea

NOT NULL becomes .required(), a nullable column .nullable(), and a declared string length .maxLength(n). The generator only emits factories the installed @adonia/core actually exports, so it can never produce a call into an API that has not shipped.

What is left out of the form

Three exclusions, all §19-shaped rather than cosmetic:

  • primary keys — assigned by the database; a fillable id is a mass-assignment hole;
  • timestampsautoCreate/autoUpdate columns and createdAt/updatedAt/deletedAt, which are record bookkeeping (and deletedAt is a tombstone the soft-delete scopes own);
  • serializeAs: null attributes — password hashes and tokens are declared never-to-leave-the-server, and a form round-trips its state to the client. Add one back deliberately, with a dehydrate() that hashes it.

--fields overrides all three: naming a column explicitly is a deliberate act.

Flags

  • --fields=a,b,c — generate only these attributes, in this order. Unknown names are reported in the file header; naming only unknown ones is an error.
  • --no-table — omit the table() override and let §7.5 derive the index from the schema instead.
  • --panel=<id> — check the resource will surface on a panel that exists, validated against .adonisjs/adonia/manifest.json. A missing manifest (the app has never booted the assembler) is a warning, not an error.
  • --force — overwrite an existing resource.

When introspection is impossible

An unreachable database, an unmigrated table or a missing Lucid never fail the command. The generator falls back to what the ORM alone knows — attribute names, date columns, the primary key, hidden columns — and records the reason twice: once as a terminal warning and once as a NOTE: in the generated file’s header, because the file outlives the scrollback.


adonia:eject <page>

Replaces a scaffolded thin re-export under inertia/pages/adonia/ with the full packaged page source (TECH_SPEC §13.4).

node ace adonia:eject resource_index
node ace adonia:eject error --force

The packaged source is read from the installed @adonia/ui (its shipped TypeScript source; the build output is a fallback, reported as a warning), and every relative import is rewritten onto the @adonia/ui package entry and merged into one value import and one type import. That is the §13.4 promise: an ejected page keeps receiving component and hook fixes through the package.

Rewriting is checked, not assumed — if a packaged page imported something the root entry does not re-export, ejection fails with the symbol name rather than writing a file that will not compile.

A page that no longer re-exports from @adonia/ui/pages is treated as yours and is not overwritten without --force.


adonia:action, adonia:widget, adonia:field, adonia:page

These four target features whose runtime lands in Phase 3 (the action DSL, the widget DSL, dashboards, the BasePage contract). Their output is written against what exists today and compiles untouched — no imports of APIs that have not shipped:

  • adonia:action <name> — a plain injectable with slug, label and handle(ctx, targets), the signature the DSL will consume. Callable and testable now.
  • adonia:widget <name> --type=value|trend|partition|table|custom — a class whose data() returns the payload shape that kind’s client component reads. The return type is written into the file, so a widget cannot quietly return a shape the renderer will not understand.
  • adonia:field <name> — a complete, working pair: a real Field subclass with its §11.1 Vine base, a real FieldProps component, and the printed r.field('<type>', Component) snippet for inertia/adonia.ts. Nothing here is waiting on Phase 3. --type sets the descriptor type string (default app/<dashed-name>); namespace it, because §13.2 registration is last-write-wins.
  • adonia:page <name> — a page class carrying the slug/component/title statics GET <panel>/pages/:page already resolves, plus an Inertia component wrapped in PanelShell. Register it with Panel.make('admin').pages([() => import('#adonia/pages/<name>_page')]) and it is reachable immediately.

Naming conventions

Post → slug posts, file post_resource.ts, labels Post/Posts. Pluralization is conventional (-y → -ies, sibilant -es, a short irregular list); the generated static slug is one editable line when it guesses wrong.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close