spree_api 6.0.0.rc3 → 6.0.0.rc5
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/controllers/concerns/spree/api/v3/admin/email_template_lookup.rb +6 -1
- data/app/controllers/concerns/spree/api/v3/admin/reporting_authorization.rb +17 -8
- data/app/controllers/concerns/spree/api/v3/admin/store_context.rb +7 -0
- data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +61 -0
- data/app/controllers/concerns/spree/api/v3/allowed_redirect_url.rb +27 -0
- data/app/controllers/concerns/spree/api/v3/cart_resolvable.rb +8 -2
- data/app/controllers/concerns/spree/api/v3/error_handler.rb +49 -0
- data/app/controllers/concerns/spree/api/v3/params_normalizer.rb +76 -7
- data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +66 -5
- data/app/controllers/spree/api/v3/admin/companies/addresses_controller.rb +7 -1
- data/app/controllers/spree/api/v3/admin/companies/tax_exemption_certificates_controller.rb +11 -5
- data/app/controllers/spree/api/v3/admin/concerns/external_references.rb +3 -0
- data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +30 -8
- data/app/controllers/spree/api/v3/admin/customers/addresses_controller.rb +4 -0
- data/app/controllers/spree/api/v3/admin/customers_controller.rb +1 -6
- data/app/controllers/spree/api/v3/admin/exports_controller.rb +15 -3
- data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +1 -8
- data/app/controllers/spree/api/v3/admin/imports_controller.rb +12 -3
- data/app/controllers/spree/api/v3/admin/markets_controller.rb +3 -1
- data/app/controllers/spree/api/v3/admin/me_controller.rb +21 -1
- data/app/controllers/spree/api/v3/admin/oauth/application_tokens_controller.rb +56 -0
- data/app/controllers/spree/api/v3/admin/oauth/applications_controller.rb +55 -0
- data/app/controllers/spree/api/v3/admin/oauth/authorizations_controller.rb +294 -0
- data/app/controllers/spree/api/v3/admin/order_groups_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +8 -0
- data/app/controllers/spree/api/v3/admin/orders/items_controller.rb +1 -0
- data/app/controllers/spree/api/v3/admin/orders/returns_controller.rb +7 -0
- data/app/controllers/spree/api/v3/admin/orders_controller.rb +20 -7
- data/app/controllers/spree/api/v3/admin/package_types_controller.rb +2 -3
- data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +3 -4
- data/app/controllers/spree/api/v3/admin/products_controller.rb +5 -3
- data/app/controllers/spree/api/v3/admin/purchase_orders_controller.rb +1 -0
- data/app/controllers/spree/api/v3/admin/reporting_controller.rb +20 -14
- data/app/controllers/spree/api/v3/admin/stock_levels_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/store_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/tags_controller.rb +8 -5
- data/app/controllers/spree/api/v3/admin/tax_exemption_reasons_controller.rb +58 -0
- data/app/controllers/spree/api/v3/admin/tax_rates_controller.rb +0 -1
- data/app/controllers/spree/api/v3/admin/translations/batches_controller.rb +17 -11
- data/app/controllers/spree/api/v3/admin/variants_controller.rb +96 -1
- data/app/controllers/spree/api/v3/oauth/authorizations_controller.rb +72 -0
- data/app/controllers/spree/api/v3/oauth/metadata_controller.rb +57 -0
- data/app/controllers/spree/api/v3/oauth/server_metadata_controller.rb +57 -0
- data/app/controllers/spree/api/v3/resource_controller.rb +36 -5
- data/app/controllers/spree/api/v3/seller/package_types_controller.rb +2 -2
- data/app/controllers/spree/api/v3/seller/products_controller.rb +7 -61
- data/app/controllers/spree/api/v3/seller/resource_controller.rb +2 -6
- data/app/controllers/spree/api/v3/store/carts/addresses_controller.rb +53 -0
- data/app/controllers/spree/api/v3/store/carts_controller.rb +7 -8
- data/app/controllers/spree/api/v3/store/companies/addresses_controller.rb +4 -0
- data/app/controllers/spree/api/v3/store/customer/addresses_controller.rb +4 -0
- data/app/controllers/spree/api/v3/store/customer/data_requests_controller.rb +19 -1
- data/app/controllers/spree/api/v3/store/customer/password_resets_controller.rb +3 -14
- data/app/controllers/spree/api/v3/store/data_request_confirmations_controller.rb +23 -0
- data/app/controllers/spree/api/v3/store/data_requests_controller.rb +56 -0
- data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +2 -11
- data/app/request_schemas/spree/api/v3/admin/email_template_preview_request_schema.rb +4 -0
- data/app/request_schemas/spree/api/v3/admin/variant_request_schema.rb +21 -0
- data/app/request_schemas/spree/api/v3/seller/price_request_schema.rb +20 -0
- data/app/request_schemas/spree/api/v3/seller/variant_request_schema.rb +19 -0
- data/app/request_schemas/spree/api/v3/variant_request_schema.rb +8 -13
- data/app/serializers/spree/api/v3/address_owner_serializer.rb +27 -0
- data/app/serializers/spree/api/v3/address_serializer.rb +5 -1
- data/app/serializers/spree/api/v3/admin/address_owner_serializer.rb +10 -0
- data/app/serializers/spree/api/v3/admin/address_serializer.rb +1 -10
- data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/catalog_assignment_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/catalog_price_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/collection_rule_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/collection_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/commission_line_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/commission_rate_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/consent_record_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/email_template_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +2 -0
- data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +2 -0
- data/app/serializers/spree/api/v3/admin/oauth_access_token_serializer.rb +62 -0
- data/app/serializers/spree/api/v3/admin/oauth_application_serializer.rb +56 -0
- data/app/serializers/spree/api/v3/admin/oauth_consent_request_serializer.rb +25 -0
- data/app/serializers/spree/api/v3/admin/oauth_protected_resource_serializer.rb +27 -0
- data/app/serializers/spree/api/v3/admin/oauth_redirect_serializer.rb +17 -0
- data/app/serializers/spree/api/v3/admin/order_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/package_type_serializer.rb +3 -3
- data/app/serializers/spree/api/v3/admin/payment_capture_event_serializer.rb +16 -0
- data/app/serializers/spree/api/v3/admin/payment_serializer.rb +12 -1
- data/app/serializers/spree/api/v3/admin/permission_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/purchase_order_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/return_serializer.rb +2 -0
- data/app/serializers/spree/api/v3/admin/seller_requirement_status_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/seller_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/seller_transfer_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/shipping_label_serializer.rb +3 -3
- data/app/serializers/spree/api/v3/admin/stock_location_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/stock_receipt_item_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/stock_receipt_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/stock_transfer_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/store_serializer.rb +1 -0
- data/app/serializers/spree/api/v3/admin/tax_exemption_certificate_serializer.rb +7 -3
- data/app/serializers/spree/api/v3/admin/tax_exemption_reason_serializer.rb +20 -0
- data/app/serializers/spree/api/v3/admin/tax_identifier_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/tax_line_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/tax_rate_serializer.rb +1 -2
- data/app/serializers/spree/api/v3/admin/variant_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +4 -1
- data/app/serializers/spree/api/v3/base_serializer.rb +41 -5
- data/app/serializers/spree/api/v3/claim_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/collection_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/company_invitation_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/company_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/data_request_confirmation_requested_event_serializer.rb +28 -0
- data/app/serializers/spree/api/v3/data_request_event_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/data_request_serializer.rb +8 -4
- data/app/serializers/spree/api/v3/delivery_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/delivery_zone_member_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/discount_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/file_upload_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/fulfillment_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/media_event_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/media_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/option_type_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/order_group_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/order_serializer.rb +4 -2
- data/app/serializers/spree/api/v3/payment_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/product_filter_availability_option_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/product_filter_option_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/seller/address_serializer.rb +3 -2
- data/app/serializers/spree/api/v3/seller/order_serializer.rb +3 -3
- data/app/serializers/spree/api/v3/seller/package_type_serializer.rb +3 -3
- data/app/serializers/spree/api/v3/seller/profile_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/seller/requirement_status_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/seller/tax_identifier_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/seller/variant_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/shipping_label_event_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/stock_movement_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/stock_receipt_event_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/variant_serializer.rb +2 -2
- data/app/services/spree/api/v3/filter_table.rb +22 -3
- data/app/services/spree/api/v3/request_schema.rb +64 -17
- data/config/locales/en.yml +1 -0
- data/config/routes.rb +80 -12
- data/lib/spree/api/agent_membership_map.rb +73 -0
- data/lib/spree/api/agent_resource_map.rb +350 -0
- data/lib/spree/api/agent_workflow_endpoints.rb +113 -0
- data/lib/spree/api/agent_write_schemas.rb +131 -0
- data/lib/spree/api/dependencies.rb +5 -4
- data/lib/spree/api/engine.rb +40 -0
- data/lib/spree/api/enum_types.rb +118 -0
- data/lib/spree/api/extension_fields.rb +167 -0
- data/lib/spree/api/oauth.rb +243 -0
- data/lib/spree/api/openapi/filter_documentation.rb +8 -1
- data/lib/spree/api/typelizer_generation.rb +5 -0
- data/lib/spree/api.rb +8 -0
- data/lib/typelizer/open_enums.rb +48 -10
- metadata +37 -6
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
# Fills {Spree::AgentTools::ResourceMap} from the Admin API's own
|
|
4
|
+
# controllers.
|
|
5
|
+
#
|
|
6
|
+
# Nothing here is a list. Every admin resource controller already declares
|
|
7
|
+
# the model it serves, the serializer it renders, the permission scope it
|
|
8
|
+
# rides and the workflow it writes through — so the map an agent reads is
|
|
9
|
+
# the API surface by construction, and a resource cannot drift from the
|
|
10
|
+
# endpoint that serves it. A controller that gains an attribute gains it
|
|
11
|
+
# for agents in the same commit.
|
|
12
|
+
#
|
|
13
|
+
# The one fact a controller does not carry is where the record lives in the
|
|
14
|
+
# dashboard, which is only used to link to it, so those are kept here.
|
|
15
|
+
#
|
|
16
|
+
# See docs/plans/6.0-mcp-server.md.
|
|
17
|
+
module AgentResourceMap
|
|
18
|
+
# Dashboard routes, for results that link to the record rather than
|
|
19
|
+
# describing where to find it. Store-relative: the panel prefixes the
|
|
20
|
+
# current store segment.
|
|
21
|
+
DASHBOARD_PATHS = {
|
|
22
|
+
'products' => '/products/%<id>s',
|
|
23
|
+
'categories' => '/products/categories/%<id>s',
|
|
24
|
+
'collections' => '/products/collections/%<id>s',
|
|
25
|
+
'price_lists' => '/products/price-lists/%<id>s',
|
|
26
|
+
'orders' => '/orders/%<id>s',
|
|
27
|
+
'customers' => '/customers/%<id>s',
|
|
28
|
+
'companies' => '/companies/%<id>s',
|
|
29
|
+
'sellers' => '/sellers/%<id>s',
|
|
30
|
+
'promotions' => '/promotions/%<id>s',
|
|
31
|
+
'channels' => '/settings/channels',
|
|
32
|
+
'markets' => '/settings/markets',
|
|
33
|
+
'payment_methods' => '/settings/payment-methods',
|
|
34
|
+
'delivery_profiles' => '/settings/delivery-profiles',
|
|
35
|
+
'stock_locations' => '/settings/stock-locations',
|
|
36
|
+
'tax_categories' => '/settings/tax-categories',
|
|
37
|
+
'tax_rates' => '/settings/tax-rates',
|
|
38
|
+
'product_types' => '/settings/product-types',
|
|
39
|
+
'commission_rates' => '/settings/commission-rates',
|
|
40
|
+
'roles' => '/settings/roles',
|
|
41
|
+
'return_reasons' => '/settings/reasons',
|
|
42
|
+
'allowed_origins' => '/settings/allowed-origins',
|
|
43
|
+
'imports' => '/settings/imports'
|
|
44
|
+
}.freeze
|
|
45
|
+
|
|
46
|
+
# Resources withheld whatever their controller says, because their
|
|
47
|
+
# serializer carries a live credential or a bearer instrument. Every
|
|
48
|
+
# tool result reaches a model and, for a hosted client, a third party.
|
|
49
|
+
#
|
|
50
|
+
# api_keys, integrations, webhook_endpoints — plaintext tokens,
|
|
51
|
+
# provider credentials and the secret that signs deliveries.
|
|
52
|
+
# gift_cards — the code IS the instrument: whoever reads it can spend
|
|
53
|
+
# the balance. A balance tool that never emits a code could be added.
|
|
54
|
+
# invitations — the acceptance link is a bearer token that mints a
|
|
55
|
+
# staff account at the invited role, and the endpoint
|
|
56
|
+
# it addresses is deliberately unauthenticated. The
|
|
57
|
+
# outbound filter drops the link, but a resource whose
|
|
58
|
+
# whole purpose is handing out authority does not
|
|
59
|
+
# belong on a read surface an agent drives.
|
|
60
|
+
WITHHELD_MODELS = %w[
|
|
61
|
+
Spree::ApiKey
|
|
62
|
+
Spree::OauthApplication
|
|
63
|
+
Spree::Integration
|
|
64
|
+
Spree::WebhookEndpoint
|
|
65
|
+
Spree::WebhookDelivery
|
|
66
|
+
Spree::GiftCard
|
|
67
|
+
Spree::Invitation
|
|
68
|
+
].freeze
|
|
69
|
+
|
|
70
|
+
# Controllers that resolve their scope per request rather than declaring
|
|
71
|
+
# one, because the scope belongs to the resource being imported or
|
|
72
|
+
# exported — creating a product import is a product write, and reading
|
|
73
|
+
# its rows exposes the uploaded data. A single static key cannot say
|
|
74
|
+
# that, so these are registered read-only under the broadest key the
|
|
75
|
+
# controller can demand, and the write path stays with the endpoint.
|
|
76
|
+
DYNAMIC_SCOPE_RESOURCES = {
|
|
77
|
+
'imports' => { model_name: 'Spree::Import', permission: 'read_settings',
|
|
78
|
+
serializer_name: 'Spree::Api::V3::Admin::ImportSerializer',
|
|
79
|
+
dashboard_path: '/settings/imports' },
|
|
80
|
+
'exports' => { model_name: 'Spree::Export', permission: 'read_settings',
|
|
81
|
+
serializer_name: 'Spree::Api::V3::Admin::ExportSerializer' }
|
|
82
|
+
}.freeze
|
|
83
|
+
|
|
84
|
+
class << self
|
|
85
|
+
# Registers this walk as how the core map fills itself, and marks the
|
|
86
|
+
# map stale so the next reader gets a fresh one. Called on every code
|
|
87
|
+
# reload; the walk itself runs when something actually reads the map.
|
|
88
|
+
#
|
|
89
|
+
# @return [void]
|
|
90
|
+
def stale!
|
|
91
|
+
Spree::AgentTools::ResourceMap.deriver = -> { install }
|
|
92
|
+
Spree::AgentTools::ResourceMap.stale!
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Walks every admin resource controller and registers what it serves.
|
|
96
|
+
#
|
|
97
|
+
# @param map [Class] defaults to the core resource map
|
|
98
|
+
# @return [Integer] how many resources were registered
|
|
99
|
+
def install(map: Spree::AgentTools::ResourceMap)
|
|
100
|
+
map.reset!
|
|
101
|
+
|
|
102
|
+
registered = resolve_collisions(controllers.filter_map { |controller| derive(controller) })
|
|
103
|
+
registered.each { |entry| map.register(**entry) }
|
|
104
|
+
|
|
105
|
+
DYNAMIC_SCOPE_RESOURCES.each { |key, attributes| map.register(key: key, **attributes) }
|
|
106
|
+
|
|
107
|
+
registered.size + DYNAMIC_SCOPE_RESOURCES.size
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Every admin resource controller in the application, the abstract
|
|
111
|
+
# bases excluded. Eager-loaded first: in development only the
|
|
112
|
+
# controllers already used would otherwise be known, and the map would
|
|
113
|
+
# differ between a fresh boot and a warm one.
|
|
114
|
+
#
|
|
115
|
+
# @return [Array<Class>]
|
|
116
|
+
def controllers
|
|
117
|
+
Rails.application.eager_load! unless Rails.application.config.eager_load
|
|
118
|
+
|
|
119
|
+
Spree::Api::V3::Admin::ResourceController.descendants.reject(&:abstract?).sort_by(&:name)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# What one controller contributes, or nil when it serves nothing an
|
|
123
|
+
# agent can address.
|
|
124
|
+
#
|
|
125
|
+
# @param controller [Class]
|
|
126
|
+
# @return [Hash, nil]
|
|
127
|
+
# Where this controller's operations live, read from the routes rather
|
|
128
|
+
# than guessed from the key: a nested or renamed resource keeps its
|
|
129
|
+
# real path, and a controller with no route is not reachable at all.
|
|
130
|
+
#
|
|
131
|
+
# The shallowest route wins. A resource reached both at the top level
|
|
132
|
+
# and under a parent (`/products` and
|
|
133
|
+
# `/catalogs/:catalog_id/products`) is addressed by the former: a tool
|
|
134
|
+
# given a product id has no parent id to fill in, and the nested route
|
|
135
|
+
# would leave `:catalog_id` in the path.
|
|
136
|
+
#
|
|
137
|
+
# @return [Hash{Symbol => String}] action => path template
|
|
138
|
+
# Across colliding controllers, the shallowest path per action.
|
|
139
|
+
def shallowest_paths(candidates)
|
|
140
|
+
candidates.flat_map { |entry| Array(entry[:api_paths]).to_a }.
|
|
141
|
+
group_by(&:first).
|
|
142
|
+
transform_values { |pairs| pairs.map(&:last).min_by { |path| [path.count(':'), path.length] } }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def api_paths_for(controller)
|
|
146
|
+
candidates = Spree::Core::Engine.routes.routes.filter_map do |route|
|
|
147
|
+
next unless route.defaults[:controller] == controller.controller_path
|
|
148
|
+
|
|
149
|
+
action = route.defaults[:action]&.to_sym
|
|
150
|
+
next if action.nil?
|
|
151
|
+
|
|
152
|
+
[action, route.path.spec.to_s.sub('(.:format)', '')]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
candidates.group_by(&:first).transform_values do |pairs|
|
|
156
|
+
pairs.map(&:last).min_by { |path| [path.count(':'), path.length] }
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def derive(controller)
|
|
161
|
+
instance = controller.allocate
|
|
162
|
+
model = safely(instance, :model_class)
|
|
163
|
+
return if model.nil? || WITHHELD_MODELS.include?(model.name)
|
|
164
|
+
|
|
165
|
+
scope = controller._scoped_resource
|
|
166
|
+
return if scope.blank?
|
|
167
|
+
|
|
168
|
+
serializer = safely(instance, :serializer_class)
|
|
169
|
+
return if serializer.nil?
|
|
170
|
+
|
|
171
|
+
key = key_for(model)
|
|
172
|
+
return unless store_scoped?(model)
|
|
173
|
+
|
|
174
|
+
create_workflow = workflow_key(instance, :create_workflow)
|
|
175
|
+
update_workflow = workflow_key(instance, :update_workflow)
|
|
176
|
+
writable = AgentWriteSchemas.attribute_names(key)
|
|
177
|
+
|
|
178
|
+
{
|
|
179
|
+
key: key,
|
|
180
|
+
model_name: model.name,
|
|
181
|
+
permission: "read_#{scope}",
|
|
182
|
+
serializer_name: serializer.name,
|
|
183
|
+
dashboard_path: DASHBOARD_PATHS[key],
|
|
184
|
+
api_paths: api_paths_for(controller),
|
|
185
|
+
write_permission: write_permission_for(model, scope, create_workflow, update_workflow, writable, controller),
|
|
186
|
+
writable_attributes: writable,
|
|
187
|
+
create_workflow_key: create_workflow,
|
|
188
|
+
update_workflow_key: update_workflow,
|
|
189
|
+
distinct: collection_distinct?(instance)
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
private
|
|
194
|
+
|
|
195
|
+
# What the controller does with its own collection. A controller that
|
|
196
|
+
# disables DISTINCT has a reason — Postgres refuses it beside an
|
|
197
|
+
# ORDER BY the select list does not carry — and a search tool adding
|
|
198
|
+
# it anyway answers with a 500.
|
|
199
|
+
def collection_distinct?(instance)
|
|
200
|
+
return true unless instance.respond_to?(:collection_distinct?, true)
|
|
201
|
+
|
|
202
|
+
instance.send(:collection_distinct?)
|
|
203
|
+
rescue StandardError
|
|
204
|
+
true
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# A model can be served by more than one controller under different
|
|
208
|
+
# permission scopes — a tax identifier through customers and through
|
|
209
|
+
# orders, media through products and through the media library. One
|
|
210
|
+
# resource key can carry only one permission, and whichever controller
|
|
211
|
+
# happened to sort last is not an answer: it would grant a key holding
|
|
212
|
+
# `read_orders` the customers' records too.
|
|
213
|
+
#
|
|
214
|
+
# So a collision resolves to the narrowest scope offered, and the
|
|
215
|
+
# resource stays read-only, because the two controllers do not agree
|
|
216
|
+
# on who may write it. A caller holding the wider scope reaches the
|
|
217
|
+
# record through the endpoint, as before.
|
|
218
|
+
def resolve_collisions(entries)
|
|
219
|
+
entries.group_by { |entry| entry[:key] }.map do |_key, candidates|
|
|
220
|
+
next candidates.first if candidates.one?
|
|
221
|
+
|
|
222
|
+
narrowest = candidates.min_by { |entry| [scope_breadth(entry[:permission]), entry[:permission]] }
|
|
223
|
+
contested = candidates.map { |entry| entry[:permission] }.uniq.size > 1
|
|
224
|
+
|
|
225
|
+
# A workflow declaration is not in dispute — whichever controller
|
|
226
|
+
# declares one, that is how the resource is written, and the
|
|
227
|
+
# refusal has to keep naming that tool. Only the generic write is
|
|
228
|
+
# withdrawn, and only when the controllers disagree about the
|
|
229
|
+
# scope guarding it.
|
|
230
|
+
narrowest.merge(
|
|
231
|
+
create_workflow_key: candidates.filter_map { |entry| entry[:create_workflow_key] }.first,
|
|
232
|
+
update_workflow_key: candidates.filter_map { |entry| entry[:update_workflow_key] }.first,
|
|
233
|
+
write_permission: contested ? nil : narrowest[:write_permission],
|
|
234
|
+
writable_attributes: contested ? [] : narrowest[:writable_attributes],
|
|
235
|
+
# One controller refusing DISTINCT is enough: its sort would
|
|
236
|
+
# make Postgres reject the query, and whichever controller
|
|
237
|
+
# happened to sort first is not an answer.
|
|
238
|
+
distinct: candidates.all? { |entry| entry[:distinct] },
|
|
239
|
+
# The shallowest route across every controller claiming this
|
|
240
|
+
# key. `narrowest` is chosen by permission breadth, which says
|
|
241
|
+
# nothing about depth — so a resource served both at the top
|
|
242
|
+
# level and under a parent kept whichever won on scope, and a
|
|
243
|
+
# tool was handed a path with an unfillable `:parent_id` in it.
|
|
244
|
+
api_paths: shallowest_paths(candidates)
|
|
245
|
+
)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# How much a scope covers, so the narrowest wins a collision. Measured
|
|
250
|
+
# by the number of resources the catalog grants it, which is the only
|
|
251
|
+
# ranking the catalog actually gives us.
|
|
252
|
+
def scope_breadth(permission)
|
|
253
|
+
_kind, scope = Spree.permissions.resolve_key(permission)
|
|
254
|
+
scope ? Array(scope.resources).size : Float::INFINITY
|
|
255
|
+
rescue StandardError
|
|
256
|
+
Float::INFINITY
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# A resource is generically writable only when the Admin API writes it
|
|
260
|
+
# by saving the record, and the OpenAPI document says what a write may
|
|
261
|
+
# set. A workflow-written resource keeps its workflow key instead (so
|
|
262
|
+
# the refusal can name the tool); a service-written one is not writable
|
|
263
|
+
# at all; and a resource with no documented body is read-only here,
|
|
264
|
+
# because a write tool with no attributes is one an agent can only get
|
|
265
|
+
# wrong.
|
|
266
|
+
# Writable when the controller documents a body and does not hand out
|
|
267
|
+
# authority.
|
|
268
|
+
#
|
|
269
|
+
# The list of service-written models this used to consult is gone
|
|
270
|
+
# (2026-10-10). It existed because a generic write assigned
|
|
271
|
+
# attributes and saved, so reaching a record whose controller
|
|
272
|
+
# orchestrates — an order, a refund, a stock level — would have
|
|
273
|
+
# skipped that orchestration. A dispatched write IS the controller,
|
|
274
|
+
# so there is nothing left to skip, and the list only withheld writes
|
|
275
|
+
# the API performs correctly.
|
|
276
|
+
#
|
|
277
|
+
# A declared workflow no longer withholds it either: the controller
|
|
278
|
+
# runs its own workflow, which is what the generic write now reaches.
|
|
279
|
+
def write_permission_for(model, scope, create_workflow, update_workflow, writable, controller)
|
|
280
|
+
return if writable.empty?
|
|
281
|
+
return if hands_out_authority?(controller)
|
|
282
|
+
|
|
283
|
+
"write_#{scope}"
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# A controller that hands out authority — roles, role grants,
|
|
287
|
+
# invitations, API keys — runs an anti-amplification check the generic
|
|
288
|
+
# write knows nothing about: a caller may only grant permissions they
|
|
289
|
+
# themselves hold. Assigning those attributes and calling `save` would
|
|
290
|
+
# skip it, so writing them stays with the endpoint that guards them.
|
|
291
|
+
#
|
|
292
|
+
# Asked of the controller rather than listed, so a surface that adopts
|
|
293
|
+
# the guard later is covered without anyone remembering this file.
|
|
294
|
+
def hands_out_authority?(controller)
|
|
295
|
+
return false unless defined?(Spree::Api::V3::Admin::RoleGrantGuard)
|
|
296
|
+
|
|
297
|
+
controller.include?(Spree::Api::V3::Admin::RoleGrantGuard)
|
|
298
|
+
rescue StandardError
|
|
299
|
+
false
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# The plural the resource is addressed by, taken from the model so two
|
|
303
|
+
# controllers serving the same model (a nested one and a top-level one)
|
|
304
|
+
# agree on it.
|
|
305
|
+
def key_for(model)
|
|
306
|
+
model.model_name.element.pluralize
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# `Spree::Base.for_store` falls back to the bare class for a model with
|
|
310
|
+
# no Store association, which would hand this store's agent every
|
|
311
|
+
# store's rows. A model that defines its own `for_store` has made a
|
|
312
|
+
# deliberate choice (customers are global by design and say so);
|
|
313
|
+
# inheriting the fallback is an accident, and a missing resource is
|
|
314
|
+
# better than a leaking one.
|
|
315
|
+
def store_scoped?(model)
|
|
316
|
+
return false unless model.respond_to?(:for_store)
|
|
317
|
+
return true if model.method(:for_store).owner != Spree::Base.singleton_class
|
|
318
|
+
|
|
319
|
+
!model.for_store(probe_store).equal?(model)
|
|
320
|
+
rescue StandardError
|
|
321
|
+
false
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# One unsaved store, reused: the probe asks what `for_store` does with
|
|
325
|
+
# a store, not with any particular one, and the walk covers every
|
|
326
|
+
# admin controller.
|
|
327
|
+
def probe_store
|
|
328
|
+
@probe_store ||= Spree::Store.new
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# The workflow a controller writes through, as its dotted key, so a
|
|
332
|
+
# generic write can name the tool that does the job instead.
|
|
333
|
+
def workflow_key(instance, method_name)
|
|
334
|
+
workflow = safely(instance, method_name)
|
|
335
|
+
workflow.respond_to?(:workflow_key) ? workflow.workflow_key : nil
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# A controller may raise from any of these — `model_class` is
|
|
339
|
+
# NotImplementedError on an abstract one, `permitted_attributes` reads
|
|
340
|
+
# request state on a few — and one such controller must not take the
|
|
341
|
+
# whole map down at boot.
|
|
342
|
+
def safely(instance, method_name)
|
|
343
|
+
instance.send(method_name)
|
|
344
|
+
rescue StandardError, NotImplementedError
|
|
345
|
+
nil
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
# Which Admin API operation runs each workflow, derived rather than listed.
|
|
4
|
+
#
|
|
5
|
+
# Walk the admin routes; for each one, read the controller source and ask
|
|
6
|
+
# whether the action behind it calls that workflow. The verb and path come
|
|
7
|
+
# from the route, and the body keys from the generated OpenAPI document —
|
|
8
|
+
# which is the only place that records a rename (a cancellation's
|
|
9
|
+
# `reason:` keyword travels as `cancel_reason_id`).
|
|
10
|
+
#
|
|
11
|
+
# Read off the controllers for the same reason {AgentResourceMap} is: a
|
|
12
|
+
# workflow that gains an endpoint is dispatched without anyone editing a
|
|
13
|
+
# file, and one whose endpoint goes away stops being dispatched.
|
|
14
|
+
module AgentWorkflowEndpoints
|
|
15
|
+
# Where a controller declares a workflow for the base class to run,
|
|
16
|
+
# rather than calling it inside an action of its own.
|
|
17
|
+
DECLARED_IN = { 'create_workflow' => 'create', 'update_workflow' => 'update' }.freeze
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# @param map [Module]
|
|
21
|
+
# @return [Integer] how many workflows were mapped
|
|
22
|
+
def install(map: Spree::AgentTools::WorkflowEndpoints)
|
|
23
|
+
map.reset!
|
|
24
|
+
keys = Spree::AgentTools::DefaultCatalog::WORKFLOWS.keys
|
|
25
|
+
document = openapi_document
|
|
26
|
+
|
|
27
|
+
admin_routes.each do |route|
|
|
28
|
+
source = controller_source(route[:controller])
|
|
29
|
+
next if source.nil?
|
|
30
|
+
|
|
31
|
+
keys.each do |key|
|
|
32
|
+
# `find` would derive, and deriving is what this is — read the
|
|
33
|
+
# entries already registered instead.
|
|
34
|
+
next if map.registered.key?(key)
|
|
35
|
+
next unless action_calling(source, key) == route[:action]
|
|
36
|
+
|
|
37
|
+
map.register(key: key, verb: route[:verb], path: route[:path],
|
|
38
|
+
body_keys: body_keys_for(document, route))
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
map.registered.size
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def admin_routes
|
|
48
|
+
Spree::Core::Engine.routes.routes.filter_map do |route|
|
|
49
|
+
controller = route.defaults[:controller].to_s
|
|
50
|
+
next unless controller.start_with?('spree/api/v3/admin')
|
|
51
|
+
|
|
52
|
+
{ controller: controller, action: route.defaults[:action].to_s,
|
|
53
|
+
verb: route.verb.to_s, path: route.path.spec.to_s.sub('(.:format)', '') }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Which action calls this workflow: the `def` nearest above the call.
|
|
58
|
+
# A declaration consumed by the base class names the action it serves
|
|
59
|
+
# instead.
|
|
60
|
+
def action_calling(source, key)
|
|
61
|
+
index = source.index("Spree.#{key}")
|
|
62
|
+
return if index.nil?
|
|
63
|
+
|
|
64
|
+
enclosing = source[0...index].scan(/^\s+def ([a-z_]+)/).flatten.last
|
|
65
|
+
DECLARED_IN.fetch(enclosing, enclosing)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def controller_source(controller_path)
|
|
69
|
+
@sources ||= {}
|
|
70
|
+
return @sources[controller_path] if @sources.key?(controller_path)
|
|
71
|
+
|
|
72
|
+
@sources[controller_path] = begin
|
|
73
|
+
klass = "#{controller_path.camelize}Controller".safe_constantize
|
|
74
|
+
file = klass && Object.const_source_location(klass.name)&.first
|
|
75
|
+
file && File.exist?(file) ? File.read(file) : nil
|
|
76
|
+
rescue StandardError
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# What the operation's request body accepts, in the names the wire
|
|
82
|
+
# uses. A workflow keyword and its body key often differ, and the
|
|
83
|
+
# document is where that difference is recorded.
|
|
84
|
+
def body_keys_for(document, route)
|
|
85
|
+
operation = document.dig('paths', openapi_path(route[:path]), route[:verb].downcase)
|
|
86
|
+
schema = operation&.dig('requestBody', 'content', 'application/json', 'schema')
|
|
87
|
+
# Bodies are declared once and published as components, so the
|
|
88
|
+
# operation points at one rather than carrying its properties.
|
|
89
|
+
schema = Spree::Api::AgentWriteSchemas.resolve_schema(schema)
|
|
90
|
+
|
|
91
|
+
(schema&.dig('properties') || {}).keys
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def openapi_path(path)
|
|
95
|
+
path.gsub(/:([a-z_]+)/) { "{#{Regexp.last_match(1)}}" }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# A packaged installation has no docs/ directory, and a body the
|
|
99
|
+
# document does not describe is simply passed through unchecked.
|
|
100
|
+
#
|
|
101
|
+
# `AgentWriteSchemas` already holds this file parsed, so it is read
|
|
102
|
+
# through there rather than a second time: the document is nearly a
|
|
103
|
+
# megabyte, and parsing it twice at boot is a second of nothing.
|
|
104
|
+
def openapi_document
|
|
105
|
+
Spree::Api::AgentWriteSchemas.document
|
|
106
|
+
rescue StandardError => e
|
|
107
|
+
Rails.logger.warn("[Spree] could not read the admin API document: #{e.class}: #{e.message}")
|
|
108
|
+
{}
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
# The writable attribute schema for each admin resource, read from the
|
|
4
|
+
# generated OpenAPI document.
|
|
5
|
+
#
|
|
6
|
+
# Not from the controllers: most of them declare their writable attributes
|
|
7
|
+
# inside `permitted_params`, where the list is a `params.permit` call that
|
|
8
|
+
# cannot be read without a request. `admin.yaml` already carries the same
|
|
9
|
+
# list — generated from the integration specs, reviewed as a diff on every
|
|
10
|
+
# API change — with the types, descriptions and examples a model needs to
|
|
11
|
+
# fill a form in correctly. Reading it means the generic write tools offer
|
|
12
|
+
# exactly the documented endpoint and nothing else.
|
|
13
|
+
#
|
|
14
|
+
# Loaded once, lazily: the document is well over a megabyte, and an
|
|
15
|
+
# installation with no agent client should never pay for parsing it.
|
|
16
|
+
module AgentWriteSchemas
|
|
17
|
+
# A collection endpoint under the admin namespace: `/api/v3/admin/markets`
|
|
18
|
+
# but not `/api/v3/admin/markets/{id}` or a nested child.
|
|
19
|
+
COLLECTION_PATH = %r{\A/api/v3/admin/(?<key>[a-z0-9_]+)\z}
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
# The writable properties for one resource.
|
|
23
|
+
#
|
|
24
|
+
# @param key [String] the resource key, e.g. "markets"
|
|
25
|
+
# @return [Hash] JSON Schema properties, empty when the resource has no
|
|
26
|
+
# documented write
|
|
27
|
+
def for(key)
|
|
28
|
+
schemas[key.to_s] || {}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [Array<String>] the attribute names a write may set
|
|
32
|
+
def attribute_names(key)
|
|
33
|
+
self.for(key).keys
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Where the generated document lives, relative to this engine. Resolved
|
|
37
|
+
# lazily: the engine class does not exist yet while this file loads.
|
|
38
|
+
#
|
|
39
|
+
# @return [Pathname]
|
|
40
|
+
def spec_path
|
|
41
|
+
Spree::Api::Engine.root.join('..', '..', 'docs', 'api-reference', 'admin.yaml')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The parsed document, for a reader that needs more of it than the
|
|
45
|
+
# write bodies — the workflow endpoint map wants the member actions'
|
|
46
|
+
# bodies too, and parsing a megabyte of YAML twice is wasteful.
|
|
47
|
+
#
|
|
48
|
+
# @return [Hash]
|
|
49
|
+
def document
|
|
50
|
+
@document ||= begin
|
|
51
|
+
path = spec_path
|
|
52
|
+
File.exist?(path) ? (YAML.unsafe_load_file(path) || {}) : {}
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
Rails.logger.warn("[Spree] could not read the admin API document: #{e.class}: #{e.message}")
|
|
55
|
+
{}
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def schemas
|
|
62
|
+
@schemas ||= load_schemas
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A missing or unparseable document is not a boot failure: the generic
|
|
66
|
+
# write tools simply offer nothing, which is the safe direction. An
|
|
67
|
+
# installation running from the packaged gems has no docs/ directory at
|
|
68
|
+
# all.
|
|
69
|
+
def load_schemas
|
|
70
|
+
path = spec_path
|
|
71
|
+
return {} unless File.exist?(path)
|
|
72
|
+
|
|
73
|
+
document = YAML.unsafe_load_file(path)
|
|
74
|
+
extract(document)
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
Rails.logger.warn("[Spree] could not read admin API write schemas: #{e.class}: #{e.message}")
|
|
77
|
+
{}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def extract(document)
|
|
81
|
+
Array(document['paths']).to_h.each_with_object({}) do |(path, operations), result|
|
|
82
|
+
match = COLLECTION_PATH.match(path)
|
|
83
|
+
next if match.nil?
|
|
84
|
+
|
|
85
|
+
properties = write_properties(operations)
|
|
86
|
+
result[match[:key]] = properties if properties.present?
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The create body where there is one, because it documents every
|
|
91
|
+
# settable attribute; an update body usually repeats a subset.
|
|
92
|
+
def write_properties(operations)
|
|
93
|
+
body = operations.dig('post', 'requestBody', 'content', 'application/json', 'schema')
|
|
94
|
+
body ||= operations.dig('patch', 'requestBody', 'content', 'application/json', 'schema')
|
|
95
|
+
|
|
96
|
+
resolve_schema(body)&.dig('properties').presence
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
public
|
|
100
|
+
|
|
101
|
+
# A request body is declared once per endpoint and published as a
|
|
102
|
+
# component, so the operation carries a `$ref` to it rather than the
|
|
103
|
+
# properties themselves. A body that builds on a shared shape is an
|
|
104
|
+
# `allOf` of that shape and its own fields, read as one object.
|
|
105
|
+
# Followed a few hops, which is as deep as the generated document
|
|
106
|
+
# nests them.
|
|
107
|
+
#
|
|
108
|
+
# @param schema [Hash, nil] a schema that may be a reference
|
|
109
|
+
# @return [Hash, nil] the schema it names
|
|
110
|
+
def resolve_schema(schema, depth = 0)
|
|
111
|
+
return unless schema.is_a?(Hash)
|
|
112
|
+
return if depth > 3
|
|
113
|
+
return merge_all_of(schema['allOf'], depth) if schema.key?('allOf')
|
|
114
|
+
return schema unless schema.key?('$ref')
|
|
115
|
+
|
|
116
|
+
name = schema['$ref'].to_s.split('/').last
|
|
117
|
+
resolve_schema(document.dig('components', 'schemas', name), depth + 1)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def merge_all_of(parts, depth)
|
|
123
|
+
parts.filter_map { |part| resolve_schema(part, depth + 1) }.each_with_object({}) do |part, merged|
|
|
124
|
+
merged['properties'] = (merged['properties'] || {}).merge(part['properties'] || {})
|
|
125
|
+
merged['required'] = (merged['required'] || []) | Array(part['required'])
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -9,7 +9,7 @@ module Spree
|
|
|
9
9
|
STORE_SERIALIZERS =
|
|
10
10
|
%i[credit_card price price_history product product_type variant media option_type option_value cart order
|
|
11
11
|
order_group line_item payment payment_session payment_setup_session payment_source store_credit
|
|
12
|
-
fulfillment delivery address customer data_request customer_group country channel product_publication
|
|
12
|
+
fulfillment delivery address address_owner customer data_request customer_group country channel product_publication
|
|
13
13
|
market state wishlist wishlist_item payment_method delivery_method delivery_rate freight_summary
|
|
14
14
|
stock_location category collection applied_promotion discount tax_line delivery_zone delivery_zone_member
|
|
15
15
|
fee digital_link gift_card currency locale policy custom_field tax_category tax_identifier
|
|
@@ -32,13 +32,14 @@ module Spree
|
|
|
32
32
|
exchange exchange_line_item claim claim_line_item package_type policy tax_category tax_rate
|
|
33
33
|
tax_identifier company company_membership company_invitation catalog catalog_price catalog_price_tier
|
|
34
34
|
catalog_product price_list_product catalog_assignment catalog_quantity_rule catalog_product_term
|
|
35
|
-
catalog_order_minimum tax_exemption_certificate admin_user actor seller_team_member product_submission
|
|
35
|
+
catalog_order_minimum tax_exemption_certificate tax_exemption_reason admin_user actor seller_team_member product_submission
|
|
36
36
|
seller seller_requirement seller_requirement_submission seller_requirement_status commission_rate
|
|
37
|
-
commission_rule commission_line seller_transfer seller_payout seller_balance payment_split address
|
|
37
|
+
commission_rule commission_line seller_transfer seller_payout seller_balance payment_split payment_capture_event address address_owner
|
|
38
38
|
channel order_routing_rule product_publication market delivery_method delivery_method_service
|
|
39
39
|
stock_location stock_reservation delivery_rate freight_summary freight_summary_line payment_method
|
|
40
40
|
credit_card store_credit store_credit_event customer_group payment_source digital_asset digital_link
|
|
41
|
-
store setup_task update_status api_key
|
|
41
|
+
store setup_task update_status api_key oauth_application oauth_access_token oauth_consent_request oauth_redirect
|
|
42
|
+
oauth_protected_resource allowed_origin webhook_endpoint webhook_event webhook_delivery invitation
|
|
42
43
|
invitation_acceptance_link role permission export saved_report import import_row import_mapping promotion
|
|
43
44
|
promotion_action promotion_rule coupon_code price_adjustment_tier price_list price_rule
|
|
44
45
|
resource_translations email_template email_template_draft email_template_revision email_template_preview
|
data/lib/spree/api/engine.rb
CHANGED
|
@@ -2,6 +2,11 @@ require 'rails/engine'
|
|
|
2
2
|
|
|
3
3
|
require_relative 'dependencies'
|
|
4
4
|
require_relative 'configuration'
|
|
5
|
+
require_relative 'agent_write_schemas'
|
|
6
|
+
require_relative 'agent_resource_map'
|
|
7
|
+
require_relative 'agent_membership_map'
|
|
8
|
+
require_relative 'agent_workflow_endpoints'
|
|
9
|
+
require_relative 'oauth'
|
|
5
10
|
|
|
6
11
|
module Spree
|
|
7
12
|
module Api
|
|
@@ -14,11 +19,46 @@ module Spree
|
|
|
14
19
|
Spree::Api::Dependencies = Spree::Api::ApiDependencies.new
|
|
15
20
|
end
|
|
16
21
|
|
|
22
|
+
# Applied from the engine so an installation gets a correct, locked-down
|
|
23
|
+
# authorization server by adding the gem.
|
|
24
|
+
#
|
|
25
|
+
# Before host initializers, not after: `Doorkeeper.configure` replaces
|
|
26
|
+
# the whole configuration, so running later would silently discard a
|
|
27
|
+
# host's own `config/initializers/doorkeeper.rb`. This way the host's
|
|
28
|
+
# file wins, which is what "configure it yourself" has to mean.
|
|
29
|
+
initializer 'spree.api.oauth', before: :load_config_initializers do |_app|
|
|
30
|
+
Spree::Api::Oauth.configure!
|
|
31
|
+
|
|
32
|
+
# The Admin API as a whole, for an app that works across it rather
|
|
33
|
+
# than through one endpoint. A token naming this audience is accepted
|
|
34
|
+
# on every admin controller; one naming a narrower resource — the MCP
|
|
35
|
+
# endpoint, say — is not, so a token taken from an MCP client cannot
|
|
36
|
+
# be replayed against the rest of the API.
|
|
37
|
+
Spree::Api::Oauth.register_resource(:admin, '/api/v3/admin')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Extensions register their permission scopes from config initializers,
|
|
41
|
+
# which run after the configuration above — so the grantable list is
|
|
42
|
+
# re-read once they have.
|
|
43
|
+
config.after_initialize do
|
|
44
|
+
Spree::Api::Oauth.refresh_scopes!
|
|
45
|
+
end
|
|
46
|
+
|
|
17
47
|
initializer 'spree.api.request_size_limit' do |app|
|
|
18
48
|
require_relative 'middleware/request_size_limit'
|
|
19
49
|
app.middleware.insert_before Rack::Runtime, Spree::Api::Middleware::RequestSizeLimit
|
|
20
50
|
end
|
|
21
51
|
|
|
52
|
+
# The resources agents may read and write are derived from the admin
|
|
53
|
+
# controllers themselves (see Spree::Api::AgentResourceMap). Marked
|
|
54
|
+
# stale on every reload rather than rebuilt: deriving it eager-loads the
|
|
55
|
+
# application, and doing that on each file save would turn Spree's
|
|
56
|
+
# development reload from lazy into fully eager for everyone, whether or
|
|
57
|
+
# not they run an agent client. The first caller pays instead.
|
|
58
|
+
config.to_prepare do
|
|
59
|
+
Spree::Api::AgentResourceMap.stale!
|
|
60
|
+
end
|
|
61
|
+
|
|
22
62
|
# Add API event subscribers
|
|
23
63
|
config.after_initialize do
|
|
24
64
|
Spree.subscribers << Spree::WebhookEventSubscriber
|