generate:resource
Psychic considers a resource to be something that is both a database model and an entity that is accessible via an endpoint. Not all of the models you generate may need to have API endpoints for accessing them, so you will not need resource generators for these, and can instead lean on the model generators.
See the resource generator documentation for details. The CLI documentation follows:
Usage
pnpm psy g:resource [options] <path> <modelName> [columnsWithTypes...]
Arguments
-
<path>: URL path from root domain. Specify nesting resource with{}, e.g.tickets/{}/comments. -
<modelName>: The name of the model to create, e.g.PostorSettings/CommunicationPreferences. -
[columnsWithTypes...]: Space separated snake-case (except for belongs_to model name) properties like this:title:citext subtitle:string body_markdown:text style:enum:post_styles:formal,informal User:belongs_toAll properties default to not nullable; null can be allowed by appending
:optional:subtitle:string:optionalSupported types
uuid,uuid[]: a column optimized for storing UUIDscitext,citext[]: case insensitive text (indexes and queries are automatically case insensitive)encrypted: encrypted text (used in conjunction with the@deco.Encrypteddecorator)string,string[]: varchar; allowed length defaults to 255, but may be customized, e.g.:subtitle:string:128orsubtitle:string:128:optionaltext,text[]date,date[]datetime,datetime[]time,time[]timetz,timetz[]integer,integer[]decimal,decimal[]: precision,scale is required, e.g.:volume:decimal:3,2orvolume:decimal:3,2:optional- Leveraging arrays, add the "[]" suffix, e.g.:
volume:decimal[]:3,2
- Leveraging arrays, add the "[]" suffix, e.g.:
enum,enum[]: include the enum name to automatically create the enum:type:enum:room_types:bathroom,kitchen,bedroomortype:enum:room_types:bathroom,kitchen,bedroom:optional- Omit the enum values to leverage an existing enum (omits the enum type creation):
type:enum:room_typesortype:enum:room_types:optional - Leveraging arrays, add the "[]" suffix, e.g.:
type:enum[]:room_types:bathroom,kitchen,bedroom
- Omit the enum values to leverage an existing enum (omits the enum type creation):
belongs_to: not only adds a foreign key to the migration, but also adds a BelongsTo association to the generated model:- Include the fully qualified model name, e.g., if the Coach model is in
src/app/models/Health/Coach:Health/Coach:belongs_to - Aliased FK shorthand — append
@aliasto generate a foreign key with a custom column name. This is the canonical pattern for_bycolumns (e.g.canceled_by_id) and for multiple foreign keys to the same model:InternalUser@canceled_by:belongs_to:optionalproduces acanceled_by_idcolumn, acanceledByIdmodel property, and acanceledByassociation — all from one token. Examples:User@created_by:belongs_to—created_by_idFK,createdByassociationMessage@last_inbound:belongs_to:optional—last_inbound_idFK,lastInboundassociationMessage@last_outbound:belongs_to:optional—last_outbound_idFK,lastOutboundassociation
- Include the fully qualified model name, e.g., if the Coach model is in
<path>, <modelName>, and --owning-model are independent. A nested route path such as v1/host/places/{}/rooms does not mean the model should be namespaced under Place; the model name should still describe what the record is. Nested {} paths must pass --owning-model so the generated controller can query and create through the parent association.
The generator includes id, created_at, updated_at, deleted_at, and @SoftDelete() by default. Use --no-soft-delete only when hard deletion is an intentional part of the model design.
Nested resources — any <path> containing a {} parent-id placeholder — must pass --owning-model set to the fully-qualified parent model name (e.g. --owning-model=Place for v1/host/places/{}/rooms). This makes the generated controller scope every query and write through associationQuery/createAssociation on the owning model, and scaffolds the parent correctly throughout, including the generated controller spec. Omitting it produces a controller that doesn't scope to the parent, and a spec that references a parent it never declares (placeId: place.id with no place). On admin paths, which drop owner scoping, --owning-model is also how you reintroduce it.
g:resource unconditionally regenerates the model file, unit spec, factory, and serializer for the given model name — it does not check whether those files already exist, and there is no flag to skip them (--only controls which controller actions are scaffolded, not which files are written). Running g:resource for a model name that already has hand-edited associations, hooks, validations, or serializer fields will silently discard those edits. To add a controller and routes to a model that already exists, commit your work first, so you can discard the regenerated model, spec, factory, and serializer, keep only the new controller, controller spec, and route entry, and delete the generated create-… migration (the table already exists).
Options
--singular: Generates a "resource" route instead of "resources", along with the necessary controller and spec changes. Choose it by association shape, not naming convention: pass--singularwhen the owning side has-one of the resource (e.g. aUserHasOneHost). It generatesr.resourceinstead ofr.resourcesand omits theindexaction —r.resourcedeclares the path without:idbecause there's only one, so there's nothing to disambiguate by ID. Getting this wrong produces a controller/spec that don't match reality: a plural resource generated against aHasOneparent callsassociationQuery('hosts')when the model only has a singularhostassociation, which won't compile.--only <onlyActions>: Comma separated list of resourceful endpoints (e.g.--only=create,show); any of:indexcreateshowupdatedestroy
--sti-base-serializer: Generates an STI-aware base serializer that child serializers can extend. Use this when the generated model is an STI parent.--owning-model <modelName>: The model class of the object thatassociationQuery/createAssociationwill be performed on in the created controller and spec (e.g., "Host", "Guest", "Ticketing/Ticket"). Defaults tothis.currentUser, and tothis.currentInternalUserfor paths beginning withinternal. For paths beginning withadmin, there is no default: the generated controller queries the model directly, so every admin user can access it. Required for nested resources — see the caution above.--no-soft-delete: Skip the default@SoftDelete()decorator anddeleted_atcolumn, sodestroy()permanently removes rows instead of marking them removed.--connection-name <connectionName>: The name of the db connection you would like to use for your model. Defaults to "default" (default: "default").--table-name <tableName>: Explicit table name to use instead of the auto-generated one, for when model namespaces produce long or awkward table names (e.g.--table-name=notif_prefs).--model-name <modelName>: Explicit model class name to use instead of the auto-generated one (e.g.--model-name=KitchenforRoom/Kitchen).-h, --help: Display help for command.
After generating
Once the resource is generated:
- Update the migration file as needed (e.g., add
unique()to a column), then runpnpm psy db:migrate. - Update the generated controller spec first, then the corresponding generated controller. Generated action bodies start commented out, and an action that sends no response returns 404 — so a generated spec that expects success fails with 404 until its action is filled in.