Jak używać
- Skopiuj polecenie instalacji i sprawdź źródła przed instalacją we własnym środowisku agenta.
- W oryginale sprawdź wymagane narzędzia, dane dostępowe i obsługiwane środowiska.
- Opisz dane wejściowe i oczekiwany rezultat; porównaj wynik ze źródłami.
npx skills add https://github.com/spree/agent-skills --skill spree-resourcePrzed rozpoczęciem
- Instalacja nie obejmuje subskrypcji modelu, limitu API ani uprawnień do plików.
- Gwiazdki i instalacje pokazują aktywność, a nie niezależny test jakości lub bezpieczeństwa.
Treść z repozytorium z zachowaniem nagłówków, przykładów, kodu, tabel, linków i obrazów.
Adding a Spree Resource
Commands use the Spree CLI (spree …, Docker, Rails app inserver/). On a classic Rails app without the CLI, runbin/rails g spree:api_resource …from the app root and drop theserver/prefix from paths (seespree-project).
To add a model exposed through the v3 API, use the spree:api_resource generator. One command produces:
- The model (
server/app/models/spree/<name>.rb) and its migration - Store + Admin API controllers and serializers
- A FactoryBot factory and controller specs for both APIs
- Route lines inside the
Spree::Core::Engine.add_routesblock of your app'sconfig/routes.rb - The
read_<plural>/write_<plural>permission scope (Spree.permissions.register_scopeappended toconfig/initializers/spree.rb) and its role-editor label (config/locales/spree_<plural>.en.yml) — Admin surface only
Prerequisite: run spree eject first. Generators execute inside the web container, and only the ejected dev compose bind-mounts ./server, so generated files appear (and persist) on your host.
The one-command path
spree generate api_resource Brand name:string:index slug:string:uniq active:boolean
spree migrateOutput summary:
✓ Generated Spree::Brand API resource
Prefixed ID: brand_xxxxxxxxxx
Store API: /api/v3/store/brands (read-only)
Admin API: /api/v3/admin/brands (full CRUD)Field syntax follows Rails' attribute parser, with Spree conventions applied:
| You write | You get |
|---|---|
name:string / :text / :integer / :decimal / :boolean / :date / :datetime | null: false column; presence validation for non-boolean fields |
name:string:index | Non-unique index |
slug:string:uniq | Unique index + uniqueness validation — scoped to store_id when the model is store-scoped (the default), plus spree_base_uniqueness_scope |
brand:belongs_to (or :references) | belongs_to with class_name, index, no FK constraint, null: false |
category:belongs_to{TaxCategory} | Explicit class hint (unqualified names only) → class_name: 'TaxCategory', resolved inside the Spree namespace |
description:rich_text | has_spree_rich_text + Spree::SanitizableRichText |
logo:attachment / photos:attachments | has_one_attached / has_many_attached |
belongs_to class resolution: user → Spree.customer_class; admin_user, created_by, approver, canceler → Spree.admin_user_class; anything else → Spree::<Camelized>.
Flags
| Flag | Default | Effect |
|---|---|---|
--writable | off | Store API gets create/update/destroy. Default Store API is read-only (index + show). |
--no-store | — | Skip the Store API controller + serializer + route. Admin-only resource. |
--no-admin | — | Skip the Admin API surface. Rare. |
--store-name=Discount | — | Expose the Store API under a different name (/api/v3/store/discounts); model, table and Admin API keep the canonical name. |
--no-store-scoped | scoped | By default the model includes Spree::SingleStoreResource (store_id column, belongs_to :store, auto-assigned from Spree::Current.store, for_store scope, store can't be changed later). Opt out only for global reference data. |
--no-lifecycle-events | on | By default the model calls publishes_lifecycle_events (brand.created/updated/deleted events for subscribers and webhooks). |
--custom-fields | off | Includes Spree::HasCustomFields + Spree::Metadata (merchant-defined custom fields + metadata JSON). |
--paranoid | off | acts_as_paranoid + deleted_at column/index; Admin serializer exposes deleted_at. |
--id-prefix=br | snake-cased class name | Prefixed-ID prefix (brand_… by default). Core uses short prefixes: prod_, variant_, cart_, or_, py_, ful_, cust_, sl_. |
--parent=Spree::Foo | Spree.base_class | Parent class expression. |
--permission-group=settings | catalog | Where the read_/write_ keys appear in the role editor and API-key scope picker (orders, catalog, customers, settings, … or your own lowercase identifier). |
--skip-routes | off | Don't touch routes. |
--skip-specs | off | Don't generate controller specs. |
What gets created
server/app/models/spree/brand.rb (owned-once)
server/db/migrate/<ts>_create_spree_brands.rb (append-only)
server/app/controllers/spree/api/v3/store/brands_controller.rb (managed)
server/app/controllers/spree/api/v3/admin/brands_controller.rb (managed)
server/app/serializers/spree/api/v3/brand_serializer.rb (managed)
server/app/serializers/spree/api/v3/admin/brand_serializer.rb (managed)
server/spec/factories/spree/brand_factory.rb (managed)
server/spec/controllers/spree/api/v3/{store,admin}/brands_controller_spec.rb (managed)
server/config/routes.rb (idempotent inject)
server/config/initializers/spree.rb (idempotent append — register_scope)
server/config/locales/spree_brands.en.yml (owned-once — permission label)Generated model (store-scoped default):
module Spree
class Brand < Spree.base_class
include Spree::SingleStoreResource
has_prefix_id :brand
publishes_lifecycle_events
validates :name, presence: true
validates :slug, presence: true, uniqueness: { scope: [:store_id, *spree_base_uniqueness_scope] }
self.whitelisted_ransackable_attributes = %w[name slug active]
self.whitelisted_ransackable_associations = %w[]
self.whitelisted_ransackable_scopes = %w[]
end
endRoutes. The generator inserts resources :brands, only: [:index, :show] (store) and resources :brands (admin) into the namespace :store / namespace :admin blocks inside Spree::Core::Engine.add_routes in config/routes.rb (create-spree-app / spree-starter ship this block empty). If the block or namespace is missing, it skips with a warning — add it by hand:
Rails.application.routes.draw do
Spree::Core::Engine.add_routes do
namespace :api, defaults: { format: 'json' } do
namespace :v3 do
namespace :store do
resources :brands, only: [:index, :show]
end
namespace :admin do
resources :brands
end
end
end
end
mount Spree::Core::Engine, at: '/'
endThe "owned-once / managed / append-only" contract
- Model (owned-once) — written once; re-runs never touch it. Your validations, scopes, associations live here.
- Migration (append-only) — generated once. Change schema with a new migration:
spree generate migration AddLogoToSpreeBrands logo_url:string. - Controllers, serializers, factory, specs (managed) — overwritten on re-run. If you hand-edit them, don't re-run the generator for that resource (or re-apply your edits).
- Routes and permission scope (idempotent) — a route line or
register_scope(:brands, …)already present isn't duplicated. The permission locale file is owned-once, so an edited label survives re-runs.
After running the generator
- Review the model — add validations, associations, scopes. Generated
belongs_tois required (Spree models requirebelongs_toby default; failure message is "must exist"). Addoptional: trueand relax the migration'snull: falsefor optional references. - Migrate —
spree migrate. - Grant the permission — the Admin controller declares
scoped_resource :brands, and the generator registered the matching scope inconfig/initializers/spree.rb:
# Permissions for the Brand API — grants read_brands / write_brands
# to staff roles and secret API keys. Labels: config/locales/spree_brands.en.yml
Spree.permissions.register_scope(:brands, group: :catalog, resources: -> { [Spree::Brand] })Until you grant read_brands / write_brands to a staff role (dashboard Settings → Roles, the Admin API, or seeds) or mint them on a secret key, only full-access principals get in (the admin role, read_all/write_all keys). The label lives under spree.permissions_catalog.resources.brands (label, optional description). With --no-admin nothing is registered. See spree-auth-permissions.
- Store-scoping on reads — the base controller scopes collections with
Model.for_store(current_store).SingleStoreResourceprovides that scope; for--no-store-scopedmodelsfor_storefalls back tocurrent_store.<plural>if the association exists, else the whole table. Addhas_many :brands, class_name: 'Spree::Brand', dependent: :destroytoSpree::Storevia a decorator if you wantcurrent_store.brands. A permission decides whether someone may read brands, not which ones — keep lookups store-scoped. - Permitted attributes — the generated controllers declare the writable columns in
resource_permitted_attributes(not apermitted_paramsoverride), so extension-declared attributes (Spree::Brand.additional_permitted_attributes += [...]) and param normalization still apply:
def resource_permitted_attributes
[:name, :slug, :active]
endEdit the list to drop columns clients shouldn't write — but the controllers are managed, so a re-run restores the template.
- Run the specs —
spree rspec spec/controllers/spree/api/v3/. The specs use the'API v3 Store guest'/'API v3 Admin authenticated'shared contexts fromspree_api's testing support (seespree-testing).
Adding a status
Spree has no state machines. For a record with a lifecycle, add a string status column and use Spree::HasStatus; transitions belong in workflows (see spree-workflows):
include Spree::HasStatus
has_status :draft, :active, :archived, default: :draft # predicates (active?), scopes (.active), inclusion validation, .with_status(...)Extensions append statuses with Spree::Brand.add_status('paused', after: 'active'). Don't use Rails enums for status.
TypeScript types
In a project, serializers you generate are app-local — they're not in the published @spree/sdk / @spree/admin-sdk types. Call custom endpoints via client.request<T>(...) with hand-written types (see spree-typescript-sdk). The generated serializer's typelize line documents the JSON types (decimals serialize as strings).
Common patterns
# Read-only catalog resource (Store GET, Admin CRUD)
spree generate api_resource Brand name:string:uniq active:boolean
# Writable customer-facing resource
spree generate api_resource SavedList label:string user:belongs_to --writable
# Admin-only back-office data
spree generate api_resource SupplierNote body:text --no-store
# Soft-delete + custom fields
spree generate api_resource Vendor name:string:uniq slug:string:uniq --paranoid --custom-fields
# Global reference data (no store_id)
spree generate api_resource Certification code:string:uniq --no-store-scopedFor a writable customer resource, remember the Store API authorizes by ownership: override scope in the Store controller to read through the customer (e.g. current_user.saved_lists), otherwise any shopper can see every record in the store.
Model only — no API surface
spree:model produces the model + migration with the same conventions (prefixed ID, Spree.base_class, store scoping, lifecycle events, null: false, no FKs, ransack allowlists) and accepts the same model flags (--paranoid, --custom-fields, --id-prefix, --parent, --no-store-scoped, --no-lifecycle-events). Unlike api_resource, it keeps Rails' test-framework hooks, so with rspec/factory_bot configured you also get a stub model spec and factory.
spree generate model BrandImage position:integer brand:belongs_toUse it when the record is internal (log, join table), only reachable through a parent's API, or you'll hand-write controllers. To add an API later, run spree generate api_resource Brand … — it detects the existing model and skips the model + migration (owned-once), generating only the API surface.
Gotchas
- Don't add FK constraints or Rails enums; keep
foreign_key: falseand string statuses. - Extending core models (e.g.
brand_idon products) is a decorator +Spree::Product.additional_permitted_attributes += [:brand_id](never<<— the array is frozen). Seespree-decorators. - Jobs, rake tasks, console: store-scoped records need a store. Set
Spree::Current.store = store(don't rely onSpree::Store.default, which can be nil). - Custom read-only member actions — add them to
read_actionsin the controller so they map toread_*instead ofwrite_*.
Where to read further
node_modules/@spree/docs/dist/developer/tutorial/model-and-api.md— end-to-end Brand walkthroughnode_modules/@spree/docs/dist/developer/customization/permissions.md—register_scopenode_modules/@spree/docs/dist/developer/customization/api.md— extending the API- Generator source:
spree/core/lib/generators/spree/{api_resource,model}/

