spree_api 5.4.0.rc3 → 5.4.0.rc4
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.
- checksums.yaml +4 -4
- data/app/services/spree/api/v3/filters_aggregator.rb +46 -11
- data/app/services/spree/webhooks/deliver_webhook.rb +25 -16
- data/app/subscribers/spree/webhook_event_subscriber.rb +18 -8
- data/config/routes.rb +1 -3
- metadata +8 -9
- data/app/controllers/spree/api/v3/store/categories/products_controller.rb +0 -37
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6509e525568a3b8ea64e7f0f798f4aa97269c1f050d8df3f8a0674fc53001935
|
|
4
|
+
data.tar.gz: 38bb74377c55de6b865eb28a1014fce6e5744714f44e8bf9484403e92d935044
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d748f15cd7c17a7c1bf1f9bc0bec2b22e4326c8a5088ab6989e5904ea2c315f0dad52035f6407a2995728e35ce2cf2b1adfb35f48a025d13007e658f4ee16ad1
|
|
7
|
+
data.tar.gz: c0c909f704cbdd2088b15fde911e7ceb4b919ea233c2145406bdac76e2e615046cd2b82a54c0992a4bdb518930123cd3e9c5b2a2fb4eb31d05bf98232a177cb7
|
|
@@ -2,13 +2,17 @@ module Spree
|
|
|
2
2
|
module Api
|
|
3
3
|
module V3
|
|
4
4
|
class FiltersAggregator
|
|
5
|
-
# @param scope [ActiveRecord::Relation] Base product scope (
|
|
5
|
+
# @param scope [ActiveRecord::Relation] Base product scope (fully filtered, including option values)
|
|
6
6
|
# @param currency [String] Currency for price range
|
|
7
7
|
# @param category [Spree::Category, nil] Optional category for default_sort and category filtering context
|
|
8
|
-
|
|
8
|
+
# @param option_value_ids [Array<String>] Currently selected option value prefixed IDs (for disjunctive facet counts)
|
|
9
|
+
# @param scope_before_options [ActiveRecord::Relation] Scope before option value filters (for disjunctive counts)
|
|
10
|
+
def initialize(scope:, currency:, category: nil, option_value_ids: [], scope_before_options: nil)
|
|
9
11
|
@scope = scope
|
|
10
12
|
@currency = currency
|
|
11
13
|
@category = category
|
|
14
|
+
@option_value_ids = option_value_ids
|
|
15
|
+
@scope_before_options = scope_before_options || scope
|
|
12
16
|
end
|
|
13
17
|
|
|
14
18
|
def call
|
|
@@ -78,25 +82,26 @@ module Spree
|
|
|
78
82
|
|
|
79
83
|
def option_type_filters
|
|
80
84
|
Spree::OptionType.filterable.includes(:option_values).order(:position).filter_map do |option_type|
|
|
81
|
-
|
|
85
|
+
# Disjunctive: count against scope WITHOUT this option type's filter
|
|
86
|
+
count_scope = disjunctive_scope_for(option_type)
|
|
87
|
+
values = option_type.option_values.for_products(count_scope).distinct.order(:position)
|
|
82
88
|
next if values.empty?
|
|
83
89
|
|
|
90
|
+
count_ids = count_scope.reorder('').distinct.pluck(:id)
|
|
91
|
+
|
|
84
92
|
{
|
|
85
93
|
id: option_type.prefixed_id,
|
|
86
94
|
type: 'option',
|
|
87
95
|
name: option_type.name,
|
|
88
96
|
label: option_type.label,
|
|
89
|
-
options: values.map { |ov| option_value_data(
|
|
97
|
+
options: values.map { |ov| option_value_data(count_ids, ov) }
|
|
90
98
|
}
|
|
91
99
|
end
|
|
92
100
|
end
|
|
93
101
|
|
|
94
|
-
def option_value_data(
|
|
95
|
-
# Count products in scope that have this option value
|
|
96
|
-
# We use a subquery approach to avoid GROUP BY conflicts when scope includes joins (like in_category)
|
|
97
|
-
# Join directly to option_value_variants for efficiency (skips joining through option_values table)
|
|
102
|
+
def option_value_data(product_ids, option_value)
|
|
98
103
|
count = Spree::Product
|
|
99
|
-
.where(id:
|
|
104
|
+
.where(id: product_ids)
|
|
100
105
|
.joins(:option_value_variants)
|
|
101
106
|
.where(Spree::OptionValueVariant.table_name => { option_value_id: option_value.id })
|
|
102
107
|
.distinct
|
|
@@ -111,8 +116,38 @@ module Spree
|
|
|
111
116
|
}
|
|
112
117
|
end
|
|
113
118
|
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
# Returns the scope with all option type filters EXCEPT the given one applied.
|
|
120
|
+
# This gives disjunctive counts: selecting Blue still shows Red's true count.
|
|
121
|
+
def disjunctive_scope_for(option_type)
|
|
122
|
+
return @scope_before_options if grouped_selected_options.empty?
|
|
123
|
+
|
|
124
|
+
other_groups = grouped_selected_options.except(option_type.id)
|
|
125
|
+
|
|
126
|
+
# If this type has selections but no other types do, use scope before any option filters
|
|
127
|
+
return @scope_before_options if other_groups.empty?
|
|
128
|
+
|
|
129
|
+
# Rebuild: start from scope before options, apply only other option types
|
|
130
|
+
scope = @scope_before_options
|
|
131
|
+
other_groups.each_value do |ov_ids|
|
|
132
|
+
matching = Spree::Variant.where(deleted_at: nil)
|
|
133
|
+
.joins(:option_value_variants)
|
|
134
|
+
.where(Spree::OptionValueVariant.table_name => { option_value_id: ov_ids })
|
|
135
|
+
.select(:product_id)
|
|
136
|
+
scope = scope.where(id: matching)
|
|
137
|
+
end
|
|
138
|
+
scope
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Group selected option value IDs by option type (cached, single query)
|
|
142
|
+
def grouped_selected_options
|
|
143
|
+
@grouped_selected_options ||= begin
|
|
144
|
+
return {} if @option_value_ids.blank?
|
|
145
|
+
|
|
146
|
+
decoded = @option_value_ids.filter_map { |id| Spree::OptionValue.decode_prefixed_id(id) }
|
|
147
|
+
return {} if decoded.empty?
|
|
148
|
+
|
|
149
|
+
Spree::OptionValue.where(id: decoded).group_by(&:option_type_id).transform_values { |ovs| ovs.map(&:id) }
|
|
150
|
+
end
|
|
116
151
|
end
|
|
117
152
|
|
|
118
153
|
def category_filter
|
|
@@ -49,22 +49,31 @@ module Spree
|
|
|
49
49
|
private
|
|
50
50
|
|
|
51
51
|
def make_request
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
52
|
+
headers = {
|
|
53
|
+
'Content-Type' => 'application/json',
|
|
54
|
+
'User-Agent' => 'Spree-Webhooks/1.0',
|
|
55
|
+
'X-Spree-Webhook-Signature' => generate_signature,
|
|
56
|
+
'X-Spree-Webhook-Timestamp' => webhook_timestamp.to_s,
|
|
57
|
+
'X-Spree-Webhook-Event' => @delivery.event_name
|
|
58
|
+
}
|
|
59
|
+
body = @delivery.payload.to_json
|
|
60
|
+
http_options = { open_timeout: TIMEOUT, read_timeout: TIMEOUT, verify_mode: ssl_verify_mode }
|
|
61
|
+
|
|
62
|
+
# SSRF protection is disabled in development so webhooks can reach
|
|
63
|
+
# localhost / host.docker.internal (the storefront running on the host).
|
|
64
|
+
if Rails.env.development?
|
|
65
|
+
uri = URI.parse(@delivery.url)
|
|
66
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
67
|
+
http.use_ssl = uri.scheme == 'https'
|
|
68
|
+
http_options.each { |k, v| http.send(:"#{k}=", v) }
|
|
69
|
+
|
|
70
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
71
|
+
headers.each { |k, v| request[k] = v }
|
|
72
|
+
request.body = body
|
|
73
|
+
http.request(request)
|
|
74
|
+
else
|
|
75
|
+
SsrfFilter.post(@delivery.url, headers: headers, body: body, http_options: http_options)
|
|
76
|
+
end
|
|
68
77
|
end
|
|
69
78
|
|
|
70
79
|
def generate_signature
|
|
@@ -21,13 +21,15 @@ module Spree
|
|
|
21
21
|
return unless Spree::Api::Config.webhooks_enabled
|
|
22
22
|
return if event.store_id.blank?
|
|
23
23
|
|
|
24
|
-
#
|
|
25
|
-
endpoints = Spree::WebhookEndpoint
|
|
24
|
+
# Only load the columns we need for matching and delivery
|
|
25
|
+
endpoints = Spree::WebhookEndpoint
|
|
26
|
+
.enabled
|
|
27
|
+
.where(store_id: event.store_id)
|
|
28
|
+
.select(:id, :subscriptions)
|
|
26
29
|
|
|
27
|
-
return if endpoints.empty?
|
|
28
|
-
|
|
29
|
-
# Queue delivery for each endpoint
|
|
30
30
|
endpoints.each do |endpoint|
|
|
31
|
+
next unless endpoint.subscribed_to?(event.name)
|
|
32
|
+
|
|
31
33
|
queue_delivery(endpoint, event)
|
|
32
34
|
end
|
|
33
35
|
rescue StandardError => e
|
|
@@ -38,17 +40,25 @@ module Spree
|
|
|
38
40
|
private
|
|
39
41
|
|
|
40
42
|
def queue_delivery(endpoint, event)
|
|
41
|
-
# Build base payload (without delivery ID)
|
|
42
43
|
payload = build_payload(event)
|
|
43
44
|
|
|
44
|
-
#
|
|
45
|
+
# Deduplicate: skip if we already have a delivery for this event + endpoint
|
|
46
|
+
if event.id.present?
|
|
47
|
+
return if Spree::WebhookDelivery.exists?(
|
|
48
|
+
webhook_endpoint_id: endpoint.id,
|
|
49
|
+
event_id: event.id
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
45
53
|
delivery = endpoint.webhook_deliveries.create!(
|
|
46
54
|
event_name: event.name,
|
|
55
|
+
event_id: event.id,
|
|
47
56
|
payload: payload
|
|
48
57
|
)
|
|
49
58
|
|
|
50
|
-
# Queue the delivery job
|
|
51
59
|
Spree::WebhookDeliveryJob.perform_later(delivery.id)
|
|
60
|
+
rescue ActiveRecord::RecordNotUnique
|
|
61
|
+
# Race condition: another thread already created this delivery — safe to ignore
|
|
52
62
|
rescue StandardError => e
|
|
53
63
|
Rails.logger.error "[Spree Webhooks] Error queuing delivery for endpoint #{endpoint.id}: #{e.message}"
|
|
54
64
|
Rails.error.report(e)
|
data/config/routes.rb
CHANGED
|
@@ -27,9 +27,7 @@ Spree::Core::Engine.add_routes do
|
|
|
27
27
|
get :filters, to: 'products/filters#index'
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
|
-
resources :categories, only: [:index, :show], id: /.+/
|
|
31
|
-
resources :products, only: [:index], controller: 'categories/products'
|
|
32
|
-
end
|
|
30
|
+
resources :categories, only: [:index, :show], id: /.+/
|
|
33
31
|
|
|
34
32
|
# Carts
|
|
35
33
|
resources :carts, only: [:index, :show, :create, :update, :destroy] do
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_api
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.4.0.
|
|
4
|
+
version: 5.4.0.rc4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vendo Connect Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rswag-specs
|
|
@@ -72,28 +72,28 @@ dependencies:
|
|
|
72
72
|
requirements:
|
|
73
73
|
- - "~>"
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: 0.
|
|
75
|
+
version: 0.11.0
|
|
76
76
|
type: :runtime
|
|
77
77
|
prerelease: false
|
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
79
|
requirements:
|
|
80
80
|
- - "~>"
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: 0.
|
|
82
|
+
version: 0.11.0
|
|
83
83
|
- !ruby/object:Gem::Dependency
|
|
84
84
|
name: spree_core
|
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
|
86
86
|
requirements:
|
|
87
87
|
- - '='
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: 5.4.0.
|
|
89
|
+
version: 5.4.0.rc4
|
|
90
90
|
type: :runtime
|
|
91
91
|
prerelease: false
|
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
93
|
requirements:
|
|
94
94
|
- - '='
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
|
-
version: 5.4.0.
|
|
96
|
+
version: 5.4.0.rc4
|
|
97
97
|
description: Spree's API
|
|
98
98
|
email:
|
|
99
99
|
- hello@spreecommerce.org
|
|
@@ -129,7 +129,6 @@ files:
|
|
|
129
129
|
- app/controllers/spree/api/v3/store/carts/payments_controller.rb
|
|
130
130
|
- app/controllers/spree/api/v3/store/carts/store_credits_controller.rb
|
|
131
131
|
- app/controllers/spree/api/v3/store/carts_controller.rb
|
|
132
|
-
- app/controllers/spree/api/v3/store/categories/products_controller.rb
|
|
133
132
|
- app/controllers/spree/api/v3/store/categories_controller.rb
|
|
134
133
|
- app/controllers/spree/api/v3/store/countries_controller.rb
|
|
135
134
|
- app/controllers/spree/api/v3/store/currencies_controller.rb
|
|
@@ -274,9 +273,9 @@ licenses:
|
|
|
274
273
|
- BSD-3-Clause
|
|
275
274
|
metadata:
|
|
276
275
|
bug_tracker_uri: https://github.com/spree/spree/issues
|
|
277
|
-
changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.0.
|
|
276
|
+
changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.0.rc4
|
|
278
277
|
documentation_uri: https://docs.spreecommerce.org/
|
|
279
|
-
source_code_uri: https://github.com/spree/spree/tree/v5.4.0.
|
|
278
|
+
source_code_uri: https://github.com/spree/spree/tree/v5.4.0.rc4
|
|
280
279
|
post_install_message:
|
|
281
280
|
rdoc_options: []
|
|
282
281
|
require_paths:
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
module Spree
|
|
2
|
-
module Api
|
|
3
|
-
module V3
|
|
4
|
-
module Store
|
|
5
|
-
module Categories
|
|
6
|
-
class ProductsController < Store::ProductsController
|
|
7
|
-
before_action :set_category
|
|
8
|
-
|
|
9
|
-
protected
|
|
10
|
-
|
|
11
|
-
def set_category
|
|
12
|
-
@category = find_category
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def scope
|
|
16
|
-
super.in_category(@category)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
def find_category
|
|
22
|
-
id = params[:category_id]
|
|
23
|
-
category_scope = Spree::Category.for_store(current_store).accessible_by(current_ability, :show)
|
|
24
|
-
category_scope = category_scope.i18n if Spree::Category.include?(Spree::TranslatableResource)
|
|
25
|
-
|
|
26
|
-
if id.to_s.start_with?('ctg_')
|
|
27
|
-
category_scope.find_by_prefix_id!(id)
|
|
28
|
-
else
|
|
29
|
-
find_with_fallback_default_locale { category_scope.i18n.find_by!(permalink: id) }
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|