@spree/docs 0.1.239 → 0.1.241
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-reference/store-api/metadata.md +6 -6
- package/dist/developer/core-concepts/architecture.md +15 -19
- package/dist/developer/core-concepts/carts.md +71 -1
- package/dist/developer/core-concepts/metafields.md +0 -2
- package/dist/developer/create-spree-app/quickstart.md +22 -19
- package/dist/developer/customization/api.md +2 -2
- package/dist/developer/customization/checkout.md +117 -0
- package/dist/developer/customization/configuration.md +76 -175
- package/dist/developer/customization/decorators.md +120 -130
- package/dist/developer/customization/metadata.md +9 -9
- package/dist/developer/customization/model-preferences.md +34 -68
- package/dist/developer/customization/permissions.md +13 -28
- package/dist/developer/customization/validations.md +16 -43
- package/dist/developer/dashboard/concepts.md +4 -2
- package/dist/developer/dashboard/customization/backend.md +5 -2
- package/dist/developer/dashboard/customization/navigation.md +1 -1
- package/dist/developer/dashboard/customization/quickstart.md +1 -1
- package/dist/developer/dashboard/overview.md +2 -2
- package/dist/developer/dashboard/plugins/distributing.md +1 -1
- package/dist/developer/dashboard/public-api.md +3 -2
- package/dist/developer/dashboard/recipes/attribute-end-to-end.md +3 -3
- package/dist/developer/dashboard/recipes/custom-form-field.md +1 -1
- package/dist/developer/dashboard/recipes/page-action-button.md +2 -2
- package/dist/developer/dashboard/recipes/sidebar-widget.md +2 -2
- package/dist/developer/dashboard/slots-catalog.md +60 -0
- package/dist/developer/getting-started/quickstart.md +25 -24
- package/dist/developer/how-to/custom-promotion.md +1 -1
- package/dist/developer/how-to/custom-search-provider.md +2 -2
- package/dist/developer/how-to/custom-stock-splitter.md +2 -2
- package/dist/developer/sdk/admin/extending.md +1 -1
- package/dist/developer/sdk/admin/quickstart.md +1 -1
- package/dist/developer/sdk/admin/resources.md +1 -1
- package/dist/developer/sdk/configuration.md +1 -1
- package/dist/developer/sdk/extending.md +1 -1
- package/dist/developer/tutorial/testing.md +45 -91
- package/package.json +1 -1
|
@@ -8,10 +8,9 @@ description: Use Spree decorators to add or modify behavior on core models, cont
|
|
|
8
8
|
> - **[Events & Subscribers](../core-concepts/events.md)** - For reacting to model changes (after save, create, update, delete)
|
|
9
9
|
> - **[Webhooks](../core-concepts/webhooks.md)** - For notifying external services when events occur
|
|
10
10
|
> - **[Dependencies](dependencies.md)** - For swapping out services, serializers, and abilities
|
|
11
|
-
> - **[
|
|
12
|
-
> - **[
|
|
13
|
-
> - **[
|
|
14
|
-
> - **[Ransack Configuration](../core-concepts/search-filtering.md#extending-ransackable-configuration)** - For adding searchable/sortable fields without model decorators
|
|
11
|
+
> - **[Dashboard Navigation](../dashboard/customization/navigation.md)** - For adding menu items
|
|
12
|
+
> - **[Dashboard Slots](../dashboard/customization/slots.md)** - For adding your own UI to an existing dashboard screen
|
|
13
|
+
> - **[Dashboard Tables](../dashboard/customization/tables.md)** - For customizing list views
|
|
15
14
|
|
|
16
15
|
## When to Use Decorators vs Modern Alternatives
|
|
17
16
|
|
|
@@ -23,20 +22,28 @@ Before reaching for a decorator, check if your use case is better served by a mo
|
|
|
23
22
|
| Notify external service on changes | Model decorator with callbacks | [Webhooks](../core-concepts/webhooks.md) |
|
|
24
23
|
| Custom add-to-cart logic | Service decorator | [Dependencies injection](dependencies.md) |
|
|
25
24
|
| Custom API responses | Serializer decorator | [Dependencies injection](dependencies.md) |
|
|
26
|
-
| Add
|
|
27
|
-
| Add section to
|
|
28
|
-
|
|
|
25
|
+
| Add a dashboard menu item | Controller decorator | [Navigation registry](../dashboard/customization/navigation.md) |
|
|
26
|
+
| Add a section to a dashboard form | View decorator/override | [Slots](../dashboard/customization/slots.md) |
|
|
27
|
+
| Narrow what an endpoint returns | Overriding an action | Controller decorator overriding `scope` |
|
|
28
|
+
| Accept a new attribute on write | Controller decorator | `Model.additional_permitted_attributes +=` |
|
|
29
29
|
| Add association to core model | - | Decorator (still appropriate) |
|
|
30
30
|
| Add validation to core model | - | Decorator (still appropriate) |
|
|
31
31
|
| Add new method to core model | - | Decorator (still appropriate) |
|
|
32
|
+
| Make a new column [filterable](../core-concepts/search-filtering.md) | - | Decorator (still appropriate) |
|
|
32
33
|
|
|
33
34
|
> **INFO:** Decorators are still appropriate for **structural changes** like adding associations, validations, scopes, and new methods to models. Use modern alternatives for **behavioral changes** like callbacks, hooks, and side effects.
|
|
34
35
|
|
|
35
36
|
## Overview
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
Spree's models, API controllers and helpers can be extended or overridden to meet your requirements using standard Ruby idioms.
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
The convention is a file under `server/app/models/spree` or `server/app/controllers/spree`, named after the original class with `_decorator` appended. The generators below place it for you.
|
|
41
|
+
|
|
42
|
+
> **NOTE:** There is nothing else to decorate. The dashboard is a React application that
|
|
43
|
+
> talks to the Admin API, and the storefront is your own — so the only
|
|
44
|
+
> controllers Spree ships are the API ones, and the only views are the emails.
|
|
45
|
+
> Customize the dashboard through
|
|
46
|
+
> [its own extension points](../dashboard/customization/quickstart.md).
|
|
40
47
|
|
|
41
48
|
## Why Use Decorators?
|
|
42
49
|
|
|
@@ -88,7 +95,7 @@ Spree provides generators to create decorator files with the correct structure:
|
|
|
88
95
|
|
|
89
96
|
|
|
90
97
|
```bash Spree CLI (Docker)
|
|
91
|
-
spree generate
|
|
98
|
+
spree generate model_decorator Spree::Product
|
|
92
99
|
```
|
|
93
100
|
|
|
94
101
|
```bash Without Spree CLI
|
|
@@ -96,7 +103,7 @@ bin/rails g spree:model_decorator Spree::Product
|
|
|
96
103
|
```
|
|
97
104
|
|
|
98
105
|
|
|
99
|
-
This creates `app/models/spree/product_decorator.rb`:
|
|
106
|
+
This creates `server/app/models/spree/product_decorator.rb`:
|
|
100
107
|
|
|
101
108
|
```ruby
|
|
102
109
|
module Spree
|
|
@@ -114,18 +121,18 @@ end
|
|
|
114
121
|
|
|
115
122
|
|
|
116
123
|
```bash Spree CLI (Docker)
|
|
117
|
-
spree generate
|
|
124
|
+
spree generate controller_decorator Spree::Api::V3::Admin::ProductsController
|
|
118
125
|
```
|
|
119
126
|
|
|
120
127
|
```bash Without Spree CLI
|
|
121
|
-
bin/rails g spree:controller_decorator Spree::Admin::ProductsController
|
|
128
|
+
bin/rails g spree:controller_decorator Spree::Api::V3::Admin::ProductsController
|
|
122
129
|
```
|
|
123
130
|
|
|
124
131
|
|
|
125
|
-
This creates `app/controllers/spree/admin/products_controller_decorator.rb`:
|
|
132
|
+
This creates `server/app/controllers/spree/api/v3/admin/products_controller_decorator.rb`:
|
|
126
133
|
|
|
127
134
|
```ruby
|
|
128
|
-
module Spree::Admin
|
|
135
|
+
module Spree::Api::V3::Admin
|
|
129
136
|
module ProductsControllerDecorator
|
|
130
137
|
def self.prepended(base)
|
|
131
138
|
# base.before_action :my_filter
|
|
@@ -135,7 +142,7 @@ module Spree::Admin
|
|
|
135
142
|
end
|
|
136
143
|
end
|
|
137
144
|
|
|
138
|
-
Spree::Admin::ProductsController.prepend Spree::Admin::ProductsControllerDecorator
|
|
145
|
+
Spree::Api::V3::Admin::ProductsController.prepend Spree::Api::V3::Admin::ProductsControllerDecorator
|
|
139
146
|
```
|
|
140
147
|
|
|
141
148
|
The generator accepts any namespace depth. `Spree::ProductsController` produces a top-level `module Spree` wrapper; `Spree::Api::V3::Store::ProductsController` produces a `module Spree::Api::V3::Store` wrapper. The final `.prepend` line is always fully qualified.
|
|
@@ -146,7 +153,7 @@ The generator accepts any namespace depth. `Spree::ProductsController` produces
|
|
|
146
153
|
|
|
147
154
|
The most common use case is changing the behavior of existing methods. When overriding a method, you can call `super` to invoke the original implementation:
|
|
148
155
|
|
|
149
|
-
```ruby app/models/spree/product_decorator.rb
|
|
156
|
+
```ruby server/app/models/spree/product_decorator.rb
|
|
150
157
|
module Spree
|
|
151
158
|
module ProductDecorator
|
|
152
159
|
def available?
|
|
@@ -168,7 +175,7 @@ end
|
|
|
168
175
|
|
|
169
176
|
Add new instance methods directly in the decorator module:
|
|
170
177
|
|
|
171
|
-
```ruby app/models/spree/product_decorator.rb
|
|
178
|
+
```ruby server/app/models/spree/product_decorator.rb
|
|
172
179
|
module Spree
|
|
173
180
|
module ProductDecorator
|
|
174
181
|
def featured?
|
|
@@ -189,7 +196,7 @@ end
|
|
|
189
196
|
|
|
190
197
|
Use the `self.prepended(base)` callback to add associations:
|
|
191
198
|
|
|
192
|
-
```ruby app/models/spree/product_decorator.rb
|
|
199
|
+
```ruby server/app/models/spree/product_decorator.rb
|
|
193
200
|
module Spree
|
|
194
201
|
module ProductDecorator
|
|
195
202
|
def self.prepended(base)
|
|
@@ -204,7 +211,7 @@ end
|
|
|
204
211
|
|
|
205
212
|
### Adding Validations
|
|
206
213
|
|
|
207
|
-
```ruby app/models/spree/product_decorator.rb
|
|
214
|
+
```ruby server/app/models/spree/product_decorator.rb
|
|
208
215
|
module Spree
|
|
209
216
|
module ProductDecorator
|
|
210
217
|
def self.prepended(base)
|
|
@@ -219,7 +226,7 @@ end
|
|
|
219
226
|
|
|
220
227
|
### Adding Scopes
|
|
221
228
|
|
|
222
|
-
```ruby app/models/spree/product_decorator.rb
|
|
229
|
+
```ruby server/app/models/spree/product_decorator.rb
|
|
223
230
|
module Spree
|
|
224
231
|
module ProductDecorator
|
|
225
232
|
def self.prepended(base)
|
|
@@ -237,7 +244,7 @@ end
|
|
|
237
244
|
|
|
238
245
|
Use `extend` within the `prepended` callback to add class methods:
|
|
239
246
|
|
|
240
|
-
```ruby app/models/spree/product_decorator.rb
|
|
247
|
+
```ruby server/app/models/spree/product_decorator.rb
|
|
241
248
|
module Spree
|
|
242
249
|
module ProductDecorator
|
|
243
250
|
def self.prepended(base)
|
|
@@ -263,131 +270,103 @@ Spree::Product.search_by_name('shirt')
|
|
|
263
270
|
|
|
264
271
|
## Decorating Controllers
|
|
265
272
|
|
|
266
|
-
|
|
273
|
+
The only controllers Spree ships are the API ones, under
|
|
274
|
+
`Spree::Api::V3::Store` and `Spree::Api::V3::Admin`. There is no server-rendered
|
|
275
|
+
storefront or admin to decorate — the dashboard is a React app that talks to the
|
|
276
|
+
Admin API, and the storefront is yours.
|
|
267
277
|
|
|
268
|
-
|
|
278
|
+
> **WARNING:** **Reach for a decorator last.** A `ResourceController` subclass exposes named
|
|
279
|
+
> hooks — `scope`, `permitted_params`, `serializer_class`, `collection_includes`
|
|
280
|
+
> — and overriding one of those in a decorator is safe. Overriding an *action*
|
|
281
|
+
> couples your code to Spree's internals and is what breaks on upgrade.
|
|
269
282
|
|
|
270
|
-
|
|
283
|
+
### Narrowing what an endpoint returns
|
|
271
284
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
class ProductQuickViewsController < StoreController
|
|
275
|
-
def show
|
|
276
|
-
@product = current_store.products.friendly.find(params[:product_id])
|
|
277
|
-
render partial: 'spree/products/quick_view', locals: { product: @product }
|
|
278
|
-
end
|
|
279
|
-
end
|
|
280
|
-
end
|
|
281
|
-
```
|
|
285
|
+
The most common reason to decorate: restrict a listing beyond what the store
|
|
286
|
+
scope already does.
|
|
282
287
|
|
|
283
|
-
```
|
|
284
|
-
Spree::
|
|
285
|
-
get 'products/:product_id/quick_view', to: 'product_quick_views#show', as: :product_quick_view
|
|
286
|
-
end
|
|
288
|
+
```bash
|
|
289
|
+
spree generate controller_decorator Spree::Api::V3::Admin::ProductsController
|
|
287
290
|
```
|
|
288
291
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
- Is easier to test in isolation
|
|
292
|
-
- Makes your customizations clearly visible in your codebase
|
|
293
|
-
|
|
294
|
-
### Adding a New Action via Decorator
|
|
295
|
-
|
|
296
|
-
If you must add an action to an existing controller:
|
|
297
|
-
|
|
298
|
-
```ruby app/controllers/spree/products_controller_decorator.rb
|
|
299
|
-
module Spree
|
|
292
|
+
```ruby server/app/controllers/spree/api/v3/admin/products_controller_decorator.rb
|
|
293
|
+
module Spree::Api::V3::Admin
|
|
300
294
|
module ProductsControllerDecorator
|
|
301
|
-
|
|
302
|
-
base.before_action :load_product, only: [:quick_view]
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
def quick_view
|
|
306
|
-
respond_to do |format|
|
|
307
|
-
format.html { render partial: 'quick_view', locals: { product: @product } }
|
|
308
|
-
format.json { render json: @product }
|
|
309
|
-
end
|
|
310
|
-
end
|
|
295
|
+
protected
|
|
311
296
|
|
|
312
|
-
|
|
297
|
+
# `super` is already scoped to the current store. Chain onto it — never
|
|
298
|
+
# replace it, or the endpoint stops being scoped and starts returning
|
|
299
|
+
# another store's records. `current_user` is nil for a secret-key
|
|
300
|
+
# request, hence `&.` — a caller that is not a staff member gets the
|
|
301
|
+
# narrowed scope.
|
|
302
|
+
def scope
|
|
303
|
+
return super if current_user&.spree_admin?
|
|
313
304
|
|
|
314
|
-
|
|
315
|
-
@product = current_store.products.friendly.find(params[:id])
|
|
305
|
+
super.where(discontinue_on: nil)
|
|
316
306
|
end
|
|
317
307
|
end
|
|
318
|
-
|
|
319
|
-
ProductsController.prepend(ProductsControllerDecorator)
|
|
320
308
|
end
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
Don't forget to add the route:
|
|
324
309
|
|
|
325
|
-
|
|
326
|
-
Spree::
|
|
327
|
-
|
|
328
|
-
end
|
|
310
|
+
Spree::Api::V3::Admin::ProductsController.prepend(
|
|
311
|
+
Spree::Api::V3::Admin::ProductsControllerDecorator
|
|
312
|
+
)
|
|
329
313
|
```
|
|
330
314
|
|
|
331
|
-
|
|
315
|
+
`current_user` is the signed-in staff member, and `spree_admin?` asks whether
|
|
316
|
+
they hold the admin role for the current store.
|
|
332
317
|
|
|
333
|
-
> **WARNING:**
|
|
318
|
+
> **WARNING:** `current_user` is only set when the request carries a staff token. A request
|
|
319
|
+
> authenticated with a [secret API key](../../api-reference/admin-api/authentication.md)
|
|
320
|
+
> has no user, so `current_user` is `nil` — guard for it, or a server-to-server
|
|
321
|
+
> integration hits `NoMethodError` on every call.
|
|
334
322
|
|
|
335
|
-
|
|
336
|
-
module Spree
|
|
337
|
-
module Admin
|
|
338
|
-
module ProductsControllerDecorator
|
|
339
|
-
def create
|
|
340
|
-
# Add custom logic before
|
|
341
|
-
log_product_creation_attempt
|
|
342
|
-
|
|
343
|
-
# Call original method
|
|
344
|
-
super
|
|
323
|
+
### Accepting an extra attribute
|
|
345
324
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
325
|
+
If your decorator added a column to a core model, the controller has to permit
|
|
326
|
+
it — but you rarely need a decorator for that. Extensions append to the model's
|
|
327
|
+
own list from an initializer:
|
|
349
328
|
|
|
350
|
-
|
|
329
|
+
```ruby server/config/initializers/spree.rb
|
|
330
|
+
Spree::Product.additional_permitted_attributes += [:brand_id]
|
|
331
|
+
```
|
|
351
332
|
|
|
352
|
-
|
|
353
|
-
Rails.logger.info "Product creation attempted by #{current_spree_user.email}"
|
|
354
|
-
end
|
|
333
|
+
Always `+=`, never `=`, or you drop what another extension added.
|
|
355
334
|
|
|
356
|
-
|
|
357
|
-
ProductNotificationJob.perform_later(@product)
|
|
358
|
-
end
|
|
359
|
-
end
|
|
335
|
+
### A new endpoint is a new controller
|
|
360
336
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
end
|
|
364
|
-
```
|
|
337
|
+
To add an action, write your own controller rather than decorating one of
|
|
338
|
+
Spree's. It inherits the same pagination, filtering and authorization:
|
|
365
339
|
|
|
366
|
-
|
|
340
|
+
```ruby server/app/controllers/spree/api/v3/admin/product_audits_controller.rb
|
|
341
|
+
module Spree::Api::V3::Admin
|
|
342
|
+
class ProductAuditsController < ResourceController
|
|
343
|
+
# Names the API-key scope this endpoint needs: `read_products` for
|
|
344
|
+
# index/show, `write_products` for everything else. Every Admin API
|
|
345
|
+
# controller must declare one.
|
|
346
|
+
scoped_resource :products
|
|
367
347
|
|
|
368
|
-
|
|
348
|
+
protected
|
|
369
349
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
module CheckoutControllerDecorator
|
|
373
|
-
def self.prepended(base)
|
|
374
|
-
base.before_action :check_minimum_order, only: [:update]
|
|
350
|
+
def model_class
|
|
351
|
+
Spree::ProductAudit
|
|
375
352
|
end
|
|
376
353
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
def check_minimum_order
|
|
380
|
-
if @order.total < 25.0 && params[:state] == 'payment'
|
|
381
|
-
flash[:error] = 'Minimum order amount is $25'
|
|
382
|
-
redirect_to checkout_state_path(@order.state)
|
|
383
|
-
end
|
|
354
|
+
def serializer_class
|
|
355
|
+
MyApp::ProductAuditSerializer
|
|
384
356
|
end
|
|
385
357
|
end
|
|
386
|
-
|
|
387
|
-
CheckoutController.prepend(CheckoutControllerDecorator)
|
|
388
358
|
end
|
|
389
359
|
```
|
|
390
360
|
|
|
361
|
+
Add the route to your application's `config/routes.rb`, inside the engine's
|
|
362
|
+
route hook. See [extending the API](api.md).
|
|
363
|
+
|
|
364
|
+
> **INFO:** **For side effects, use events rather than a decorator.** Subscribing to
|
|
365
|
+
> `product.created` runs your code for every caller — the dashboard, a secret
|
|
366
|
+
> API key, an import, the console. A controller override only fires for
|
|
367
|
+
> requests that happen to pass through that controller. See
|
|
368
|
+
> [Events](../core-concepts/events.md).
|
|
369
|
+
|
|
391
370
|
## Best Practices
|
|
392
371
|
|
|
393
372
|
|
|
@@ -405,7 +384,7 @@ end
|
|
|
405
384
|
If you have many customizations for a single class, consider splitting them into focused decorators:
|
|
406
385
|
|
|
407
386
|
```
|
|
408
|
-
app/models/spree/
|
|
387
|
+
server/app/models/spree/
|
|
409
388
|
├── product_decorator.rb # Main decorator (loads others)
|
|
410
389
|
├── product/
|
|
411
390
|
│ ├── brand_decorator.rb # Brand association
|
|
@@ -413,13 +392,22 @@ app/models/spree/
|
|
|
413
392
|
│ └── seo_decorator.rb # SEO-related methods
|
|
414
393
|
```
|
|
415
394
|
|
|
416
|
-
```ruby app/models/spree/product_decorator.rb
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
395
|
+
```ruby server/app/models/spree/product_decorator.rb
|
|
396
|
+
module Spree
|
|
397
|
+
module ProductDecorator
|
|
398
|
+
include Product::BrandDecorator
|
|
399
|
+
include Product::InventoryDecorator
|
|
400
|
+
include Product::SeoDecorator
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
Product.prepend(ProductDecorator)
|
|
404
|
+
end
|
|
421
405
|
```
|
|
422
406
|
|
|
407
|
+
Each focused module is a plain module — only the top-level decorator calls
|
|
408
|
+
`prepend`. Autoloading resolves them from their paths, so nothing needs
|
|
409
|
+
requiring by hand.
|
|
410
|
+
|
|
423
411
|
## Common Pitfalls
|
|
424
412
|
|
|
425
413
|
### Forgetting to Call Super
|
|
@@ -475,7 +463,7 @@ If you have existing decorators that use callbacks for side effects, consider mi
|
|
|
475
463
|
|
|
476
464
|
**Before (Decorator with callback):**
|
|
477
465
|
|
|
478
|
-
```ruby app/models/spree/product_decorator.rb
|
|
466
|
+
```ruby server/app/models/spree/product_decorator.rb
|
|
479
467
|
module Spree
|
|
480
468
|
module ProductDecorator
|
|
481
469
|
def self.prepended(base)
|
|
@@ -495,7 +483,7 @@ end
|
|
|
495
483
|
|
|
496
484
|
**After (Events subscriber):**
|
|
497
485
|
|
|
498
|
-
```ruby app/subscribers/product_sync_subscriber.rb
|
|
486
|
+
```ruby server/app/subscribers/product_sync_subscriber.rb
|
|
499
487
|
class ProductSyncSubscriber < Spree::Subscriber
|
|
500
488
|
subscribes_to 'product.updated'
|
|
501
489
|
|
|
@@ -503,14 +491,16 @@ class ProductSyncSubscriber < Spree::Subscriber
|
|
|
503
491
|
product = Spree::Product.find_by_prefix_id(event.payload['id'])
|
|
504
492
|
return unless product
|
|
505
493
|
|
|
506
|
-
|
|
507
|
-
if event.payload['previous_changes']&.key?('name')
|
|
508
|
-
ExternalSyncJob.perform_later(product)
|
|
509
|
-
end
|
|
494
|
+
ExternalSyncJob.perform_later(product)
|
|
510
495
|
end
|
|
511
496
|
end
|
|
512
497
|
```
|
|
513
498
|
|
|
499
|
+
The payload is the record as the API serializes it, so it carries the new
|
|
500
|
+
values rather than a list of what changed. When a subscriber only cares about
|
|
501
|
+
one attribute, compare against what you last synced rather than looking for a
|
|
502
|
+
changeset in the event.
|
|
503
|
+
|
|
514
504
|
### Benefits of Migration
|
|
515
505
|
|
|
516
506
|
|
|
@@ -528,7 +518,7 @@ end
|
|
|
528
518
|
- [Events](../core-concepts/events.md) - Learn about Spree's event system
|
|
529
519
|
- [Webhooks](../core-concepts/webhooks.md) - HTTP callbacks for external integrations
|
|
530
520
|
- [Dependencies](dependencies.md) - Swap core services with your own
|
|
531
|
-
- [
|
|
532
|
-
- [
|
|
533
|
-
- [Extending Core Models Tutorial](../tutorial/model.md) - Step-by-step guide to connecting custom models with Spree core
|
|
521
|
+
- [Extending the API](api.md) - Add your own endpoints
|
|
522
|
+
- [Dashboard customization](../dashboard/customization/quickstart.md) - Extend the dashboard from its own React app
|
|
523
|
+
- [Extending Core Models Tutorial](../tutorial/model-and-api.md) - Step-by-step guide to connecting custom models with Spree core
|
|
534
524
|
- [Customization Overview](quickstart.md) - General customization patterns
|
|
@@ -6,11 +6,11 @@ title: Metadata
|
|
|
6
6
|
|
|
7
7
|
Metadata provides simple, unstructured key-value storage on Spree resources — similar to [Stripe's metadata](https://docs.stripe.com/api/metadata). It's ideal for storing integration IDs, tracking data, or any arbitrary information that doesn't need validation or admin UI.
|
|
8
8
|
|
|
9
|
-
Metadata is a **permanent, first-class system** in Spree. It is designed to coexist alongside [
|
|
9
|
+
Metadata is a **permanent, first-class system** in Spree. It is designed to coexist alongside [Custom Fields](../core-concepts/metafields.md) (structured, typed, admin-managed). The two systems serve different purposes and are not interchangeable — think of it as **metadata for machines, custom fields for humans**.
|
|
10
10
|
|
|
11
11
|
Metadata is **write-only** in the Store API — you can set it when creating or updating resources, but it is never returned in Store API responses. It is visible in Admin API responses for administrative use.
|
|
12
12
|
|
|
13
|
-
> **INFO:** For structured, type-safe custom attributes with admin UI support, use [
|
|
13
|
+
> **INFO:** For structured, type-safe custom attributes with admin UI support, use [Custom Fields](../core-concepts/metafields.md) instead.
|
|
14
14
|
|
|
15
15
|
## Store API
|
|
16
16
|
|
|
@@ -186,14 +186,14 @@ Metadata updates use **merge semantics** — existing keys are preserved, new ke
|
|
|
186
186
|
{ "source": "mobile_app", "campaign": "winter", "new_key": "value" }
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
-
## Metadata vs
|
|
189
|
+
## Metadata vs Custom Fields
|
|
190
190
|
|
|
191
191
|
Spree has two permanent, complementary systems for custom data. They are not interchangeable and neither is going away.
|
|
192
192
|
|
|
193
|
-
| | Metadata | [
|
|
193
|
+
| | Metadata | [Custom Fields](../core-concepts/metafields.md) |
|
|
194
194
|
|---|---|---|
|
|
195
195
|
| **Purpose** | Developer escape hatch — integration data, sync state, ad-hoc flags | Merchant-defined structured attributes with admin UI |
|
|
196
|
-
| **Schema** | Schemaless JSON — no definition required | Defined via
|
|
196
|
+
| **Schema** | Schemaless JSON — no definition required | Defined via CustomFieldDefinitions (typed, validated) |
|
|
197
197
|
| **Validation** | None — accepts any JSON-serializable data | Type-specific (text, number, boolean, rich text, JSON) |
|
|
198
198
|
| **Visibility** | Write-only in Store API, readable in Admin API | Configurable (front-end, back-end, both) |
|
|
199
199
|
| **Admin UI** | JSON preview only | Dedicated management forms |
|
|
@@ -208,7 +208,7 @@ Spree has two permanent, complementary systems for custom data. They are not int
|
|
|
208
208
|
- Any write-and-forget data that only needs to be read by backend systems
|
|
209
209
|
- Syncing state with external integrations (webhooks, ETL pipelines)
|
|
210
210
|
|
|
211
|
-
### When to use
|
|
211
|
+
### When to use custom fields
|
|
212
212
|
|
|
213
213
|
- Custom product specifications shown to customers (material, dimensions, certifications)
|
|
214
214
|
- Admin-managed fields with validation and type safety
|
|
@@ -231,7 +231,7 @@ The Store API currently supports writing metadata on:
|
|
|
231
231
|
|
|
232
232
|
The `public_metadata` column was never exposed in Store API responses and in practice served the same purpose as `private_metadata`. Calling `public_metadata=` emits a deprecation warning.
|
|
233
233
|
|
|
234
|
-
Always use the single `metadata` accessor for all schemaless key-value storage. If you need data visible to customers on the storefront, use [
|
|
234
|
+
Always use the single `metadata` accessor for all schemaless key-value storage. If you need data visible to customers on the storefront, use [Custom Fields](../core-concepts/metafields.md) instead.
|
|
235
235
|
|
|
236
236
|
```ruby
|
|
237
237
|
# Deprecated — use `metadata` instead
|
|
@@ -240,6 +240,6 @@ order.public_metadata = { 'gift_message' => 'Happy Birthday!' }
|
|
|
240
240
|
# Use metadata for internal storage
|
|
241
241
|
order.metadata = { 'gift_message' => 'Happy Birthday!' }
|
|
242
242
|
|
|
243
|
-
# Use
|
|
244
|
-
order.
|
|
243
|
+
# Use custom fields for customer-visible structured data
|
|
244
|
+
order.set_custom_field('custom.gift_message', 'Happy Birthday!')
|
|
245
245
|
```
|
|
@@ -6,8 +6,8 @@ description: Add typed configuration options to Spree models using model prefere
|
|
|
6
6
|
Model preferences allow you to easily extend Spree models with configuration options. Thanks to this you can store useful information on Spree models, eg.
|
|
7
7
|
|
|
8
8
|
```ruby
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
store.preferred_timezone = 'Europe/Warsaw'
|
|
10
|
+
store.save
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## Defining Model Preferences
|
|
@@ -16,41 +16,26 @@ To define a model preference, you need to add them to your model class.
|
|
|
16
16
|
|
|
17
17
|
Make sure to generate a migration to add the `preferences` column to the table. This column will store the preferences in a serialized format.
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
spree generate migration AddPreferencesToSpreeUsers preferences:text
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
```bash Without Spree CLI
|
|
25
|
-
bin/rails g migration AddPreferencesToSpreeUsers preferences:text
|
|
19
|
+
```bash
|
|
20
|
+
spree generate migration AddPreferencesToSpreeBrands preferences:text
|
|
26
21
|
```
|
|
27
22
|
|
|
28
|
-
|
|
29
23
|
Run the migration.
|
|
30
24
|
|
|
31
|
-
|
|
32
|
-
```bash Spree CLI (Docker)
|
|
25
|
+
```bash
|
|
33
26
|
spree migrate
|
|
34
27
|
```
|
|
35
28
|
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class Spree::User < ApplicationRecord
|
|
43
|
-
# ... you existing code ...
|
|
44
|
-
|
|
45
|
-
# include the preferable module
|
|
46
|
-
include Spree::Preferences::Preferable
|
|
47
|
-
# define the preferences
|
|
48
|
-
preference :language, :string, default: "English"
|
|
49
|
-
preference :sms_marketing, :boolean, default: false
|
|
29
|
+
```ruby server/app/models/spree/brand.rb
|
|
30
|
+
module Spree
|
|
31
|
+
class Brand < Spree.base_class
|
|
32
|
+
preference :featured, :boolean, default: false
|
|
33
|
+
preference :display_name, :string
|
|
34
|
+
end
|
|
50
35
|
end
|
|
51
36
|
```
|
|
52
37
|
|
|
53
|
-
|
|
38
|
+
Any model inheriting from `Spree.base_class` can declare preferences — the reader and writer methods come with it, so there is nothing to include. The values live in that model's `preferences` column.
|
|
54
39
|
|
|
55
40
|
## Accessing Model Preferences
|
|
56
41
|
|
|
@@ -63,59 +48,38 @@ There are several shortcut methods that are generated. They are shown below.
|
|
|
63
48
|
Query methods:
|
|
64
49
|
|
|
65
50
|
```ruby
|
|
66
|
-
|
|
51
|
+
brand.prefers_featured? # => false
|
|
67
52
|
```
|
|
68
53
|
|
|
69
54
|
Reader methods:
|
|
70
55
|
|
|
71
56
|
```ruby
|
|
72
|
-
|
|
73
|
-
|
|
57
|
+
brand.preferred_featured # => false
|
|
58
|
+
brand.preferred_display_name # => "English"
|
|
74
59
|
```
|
|
75
60
|
|
|
76
61
|
Writer methods:
|
|
77
62
|
|
|
78
63
|
```ruby
|
|
79
|
-
|
|
80
|
-
|
|
64
|
+
brand.prefers_featured = false # => false
|
|
65
|
+
brand.preferred_display_name = "English" # => "English"
|
|
81
66
|
```
|
|
82
67
|
|
|
83
|
-
> **NOTE:** Remember to run `
|
|
68
|
+
> **NOTE:** Remember to run `brand.save` after setting the preference value to save the changes to the database.
|
|
84
69
|
|
|
85
70
|
Check if a preference is available:
|
|
86
71
|
|
|
87
72
|
```ruby
|
|
88
|
-
|
|
73
|
+
brand.has_preference? :featured
|
|
89
74
|
```
|
|
90
75
|
|
|
91
76
|
### Generic Methods
|
|
92
77
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
Query method:
|
|
78
|
+
When the preference name is only known at runtime, read and write it generically:
|
|
96
79
|
|
|
97
80
|
```ruby
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
Reader methods:
|
|
103
|
-
|
|
104
|
-
```ruby
|
|
105
|
-
user.preferred(:sms_marketing) # => false
|
|
106
|
-
user.preferred(:language) # => "English"
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
```ruby
|
|
110
|
-
user.get_preference :sms_marketing
|
|
111
|
-
user.get_preference :language
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
Writer method:
|
|
115
|
-
|
|
116
|
-
```ruby
|
|
117
|
-
user.set_preference(:sms_marketing, false) # => false
|
|
118
|
-
user.set_preference(:language, "English") # => "English"
|
|
81
|
+
brand.get_preference(:featured) # => false
|
|
82
|
+
brand.set_preference(:display_name, 'Wilson')
|
|
119
83
|
```
|
|
120
84
|
|
|
121
85
|
### Accessing All Preferences
|
|
@@ -123,21 +87,23 @@ user.set_preference(:language, "English") # => "English"
|
|
|
123
87
|
You can get a hash of all stored preferences by accessing the `preferences` helper:
|
|
124
88
|
|
|
125
89
|
```ruby
|
|
126
|
-
|
|
90
|
+
brand.preferences # => { 'featured' => false, 'display_name' => 'Wilson' }
|
|
127
91
|
```
|
|
128
92
|
|
|
129
93
|
This hash will contain the value for every preference that has been defined for the model instance, whether the value is the default or one that has been previously stored.
|
|
130
94
|
|
|
131
95
|
## Models with preferences
|
|
132
96
|
|
|
133
|
-
|
|
97
|
+
Around fifty models use preferences, and the pattern is always the same: a family of subclasses that share a table but need different settings.
|
|
98
|
+
|
|
99
|
+
| Family | What the preferences hold |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `Spree::Calculator` | The numbers behind a delivery charge or a discount — flat rate, percentage, tiers |
|
|
102
|
+
| `Spree::PaymentMethod` | Gateway credentials and per-provider options |
|
|
103
|
+
| `Spree::Promotion::Rules` and `Spree::Promotion::Actions` | What a promotion matches on, and what it does |
|
|
104
|
+
| `Spree::Integration` | Per-service configuration and API keys |
|
|
105
|
+
| `Spree::PriceRules` and `Spree::DeliveryMethodRules` | When a price list or a delivery method applies |
|
|
106
|
+
| `Spree::Store`, `Spree::Channel` and `Spree::Market` | Merchant settings — timezone, units, currency behaviour |
|
|
134
107
|
|
|
135
|
-
|
|
136
|
-
* `Spree::Gateway`
|
|
137
|
-
* `Spree::PageBlock`
|
|
138
|
-
* `Spree::PageSection`
|
|
139
|
-
* `Spree::Page`
|
|
140
|
-
* `Spree::PaymentMethod`
|
|
141
|
-
* `Spree::PromotionRule`
|
|
142
|
-
* `Spree::Store`
|
|
108
|
+
This is why preferences exist rather than columns: `Spree::Calculator::FlatRate` and `Spree::Calculator::TieredPercent` are rows in one table with entirely different settings, and neither wants a column the other leaves null.
|
|
143
109
|
* `Spree::Theme`
|