spree_core 6.0.2 → 6.0.3
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/jobs/spree/addresses/geocode_address_job.rb +6 -3
- data/app/jobs/spree/variants/remove_line_item_job.rb +5 -0
- data/app/models/concerns/spree/preference_schema.rb +5 -0
- data/app/models/concerns/spree/typed_associations.rb +53 -20
- data/app/models/spree/address.rb +2 -2
- data/app/models/spree/collection.rb +7 -2
- data/app/models/spree/order.rb +1 -1
- data/app/models/spree/product.rb +15 -5
- data/app/models/spree/promotion.rb +1 -2
- data/app/services/spree/newsletter/subscribe.rb +1 -1
- data/app/services/spree/orders/create_from_cart.rb +6 -2
- data/app/tools/spree/agent_tools/create_export.rb +3 -1
- data/app/tools/spree/agent_tools/create_import.rb +1 -0
- data/app/tools/spree/agent_tools/create_media.rb +2 -1
- data/app/tools/spree/agent_tools/create_resource.rb +12 -4
- data/app/tools/spree/agent_tools/delete_resource.rb +12 -4
- data/app/tools/spree/agent_tools/describe_resource.rb +69 -6
- data/app/tools/spree/agent_tools/get_resource.rb +7 -4
- data/app/tools/spree/agent_tools/resource_write.rb +15 -2
- data/app/tools/spree/agent_tools/search_resources.rb +10 -11
- data/app/tools/spree/agent_tools/update_resource.rb +6 -6
- data/app/tools/spree/agent_tools/upload_file.rb +1 -0
- data/app/workflows/spree/carts/complete.rb +4 -1
- data/app/workflows/spree/carts/merge.rb +25 -4
- data/app/workflows/spree/orders/complete.rb +39 -14
- data/app/workflows/spree/products/activate.rb +7 -2
- data/app/workflows/spree/products/archive.rb +7 -2
- data/app/workflows/spree/products/draft.rb +7 -2
- data/app/workflows/spree/products/status_transition.rb +24 -0
- data/config/locales/en.yml +3 -0
- data/lib/generators/spree/api_resource/api_resource_generator.rb +35 -3
- data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +9 -4
- data/lib/generators/spree/api_resource/templates/admin_integration_spec.rb.tt +74 -0
- data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +11 -6
- data/lib/spree/agent_tool.rb +105 -1
- data/lib/spree/agent_tools/default_catalog.rb +16 -21
- data/lib/spree/agent_tools/legacy_tools.rb +212 -0
- data/lib/spree/agent_tools/operation_catalog.rb +206 -0
- data/lib/spree/agent_tools/operation_tool.rb +189 -0
- data/lib/spree/agent_tools/registry.rb +65 -31
- data/lib/spree/agent_tools/resource_map.rb +128 -5
- data/lib/spree/agent_tools.rb +3 -3
- data/lib/spree/core/dependencies_helper.rb +96 -40
- data/lib/spree/core/engine.rb +2 -1
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +19 -2
- data/lib/spree/testing_support/extension_rake.rb +12 -0
- data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +34 -0
- data/lib/tasks/dependencies.rake +2 -2
- metadata +23 -7
- data/lib/spree/agent_tools/workflow_endpoints.rb +0 -88
- data/lib/spree/agent_tools/workflow_schema.rb +0 -279
- data/lib/spree/agent_tools/workflow_tool.rb +0 -654
data/lib/spree/agent_tool.rb
CHANGED
|
@@ -21,6 +21,12 @@ module Spree
|
|
|
21
21
|
# end
|
|
22
22
|
# end
|
|
23
23
|
class AgentTool
|
|
24
|
+
# The parameter every generic resource tool takes for a resource that
|
|
25
|
+
# lives under another record, such as an order's items.
|
|
26
|
+
PARENT_ID_DESCRIPTION = 'For a resource reached through another record (describe_resource names ' \
|
|
27
|
+
'it as parent, e.g. an order for its items, discounts and fees): that ' \
|
|
28
|
+
'record\'s id as shown in search results'.freeze
|
|
29
|
+
|
|
24
30
|
# Declarations live in class attributes rather than plain class-level
|
|
25
31
|
# instance variables so a subclass inherits them. Subclassing a tool to
|
|
26
32
|
# tweak it is the documented extension path, and with bare ivars the
|
|
@@ -85,6 +91,26 @@ class AgentTool
|
|
|
85
91
|
declared_mutating == true
|
|
86
92
|
end
|
|
87
93
|
|
|
94
|
+
# Declares that this tool only adds — it creates a record or a file and
|
|
95
|
+
# never overwrites, removes or changes the state of one that exists.
|
|
96
|
+
# Every other mutating tool counts as destructive, so a tool that
|
|
97
|
+
# forgets to say is warned about more, never less.
|
|
98
|
+
#
|
|
99
|
+
# Unlike the declarations above, this one is not inherited: a subclass
|
|
100
|
+
# that overrides `call` may well change existing data, so it is
|
|
101
|
+
# destructive until it says otherwise itself.
|
|
102
|
+
#
|
|
103
|
+
# @return [void]
|
|
104
|
+
def additive!
|
|
105
|
+
@additive = true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @return [Boolean] whether the tool may overwrite, remove or change
|
|
109
|
+
# the state of existing data
|
|
110
|
+
def destructive?
|
|
111
|
+
mutating? && @additive != true
|
|
112
|
+
end
|
|
113
|
+
|
|
88
114
|
# @param name [Symbol]
|
|
89
115
|
# @param type [Symbol] JSON schema type
|
|
90
116
|
# @param description [String]
|
|
@@ -105,6 +131,44 @@ class AgentTool
|
|
|
105
131
|
def params
|
|
106
132
|
declared_params
|
|
107
133
|
end
|
|
134
|
+
|
|
135
|
+
# The declared params as JSON Schema, which is what an adapter hands a
|
|
136
|
+
# client. A tool built from an OpenAPI operation overrides this with the
|
|
137
|
+
# operation's own schema, nested properties included.
|
|
138
|
+
#
|
|
139
|
+
# @return [Hash]
|
|
140
|
+
def input_schema
|
|
141
|
+
properties = params.to_h do |name, options|
|
|
142
|
+
property = { 'type' => json_type(options[:type]) }
|
|
143
|
+
property['description'] = options[:description] if options[:description].present?
|
|
144
|
+
# An array must say what it holds: a client validating arguments
|
|
145
|
+
# against the schema refuses the call otherwise.
|
|
146
|
+
property['items'] = { 'type' => json_type(options[:items] || :string) } if property['type'] == 'array'
|
|
147
|
+
|
|
148
|
+
[name.to_s, property]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
{
|
|
152
|
+
'type' => 'object',
|
|
153
|
+
'properties' => properties,
|
|
154
|
+
'required' => params.select { |_name, options| options[:required] }.keys.map(&:to_s)
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
# A tool declares Ruby-ish types; JSON Schema wants its own names, and
|
|
161
|
+
# anything unrecognised is a string, which every client can render.
|
|
162
|
+
def json_type(type)
|
|
163
|
+
case type&.to_sym
|
|
164
|
+
when :boolean then 'boolean'
|
|
165
|
+
when :integer then 'integer'
|
|
166
|
+
when :number, :float, :decimal then 'number'
|
|
167
|
+
when :object, :hash then 'object'
|
|
168
|
+
when :array then 'array'
|
|
169
|
+
else 'string'
|
|
170
|
+
end
|
|
171
|
+
end
|
|
108
172
|
end
|
|
109
173
|
|
|
110
174
|
# @return [Spree::AgentTools::Context] who is asking, and where
|
|
@@ -115,7 +179,7 @@ class AgentTool
|
|
|
115
179
|
@context = context
|
|
116
180
|
end
|
|
117
181
|
|
|
118
|
-
delegate :tool_name, :description, :permission, :mutating?, :params, to: :class
|
|
182
|
+
delegate :tool_name, :description, :permission, :mutating?, :destructive?, :params, to: :class
|
|
119
183
|
|
|
120
184
|
# Runs the tool. Subclasses implement this with keyword arguments matching
|
|
121
185
|
# their declared params.
|
|
@@ -167,6 +231,46 @@ class AgentTool
|
|
|
167
231
|
|
|
168
232
|
protected
|
|
169
233
|
|
|
234
|
+
# A resource that lives under a parent — a promotion's rules, an order's
|
|
235
|
+
# payments — has no path of its own to dispatch to, so every tool refuses
|
|
236
|
+
# it the same way: naming the parent and, where the parent carries the
|
|
237
|
+
# rows as an attribute, how to write and read them there. The old answer
|
|
238
|
+
# was the endpoint's 404 for a parent id nobody supplied.
|
|
239
|
+
#
|
|
240
|
+
# @param entry [Spree::AgentTools::ResourceMap::Entry]
|
|
241
|
+
# @param verb [String] 'searched', 'fetched', 'created'…
|
|
242
|
+
# @param action [Symbol] the operation whose path runs under the parent
|
|
243
|
+
# @return [Hash]
|
|
244
|
+
def nested_resource_error(entry, verb, action)
|
|
245
|
+
# A top-level path left unfilled is missing its own id, not a parent.
|
|
246
|
+
return { error: "Pass the id of the #{entry.key.singularize.tr('_', ' ')} to use." } unless entry.nested?(action)
|
|
247
|
+
|
|
248
|
+
parent = entry.parent_key(action)
|
|
249
|
+
parent_entry = Spree::AgentTools::ResourceMap.find(parent)
|
|
250
|
+
attribute = parent_entry&.nested_typed_families&.key(entry.model_class)
|
|
251
|
+
|
|
252
|
+
owner = parent.singularize.tr('_', ' ')
|
|
253
|
+
sentences = ["#{entry.key} belong to #{owner.match?(/\A[aeiou]/) ? 'an' : 'a'} #{owner} and cannot be #{verb} on their own."]
|
|
254
|
+
# Offered only where one parent id fills the path; a resource nested
|
|
255
|
+
# twice has no generic route.
|
|
256
|
+
reachable = entry.path_for(action, id: 'id', parent_id: 'parent').present?
|
|
257
|
+
sentences << "Pass the #{owner}'s id as parent_id to reach them under it." if reachable
|
|
258
|
+
# Each suggested call is one this caller may actually make; a hint
|
|
259
|
+
# that leads straight to a permission refusal is worse than none.
|
|
260
|
+
readable = parent_entry && context.permitted?(parent_entry.permission)
|
|
261
|
+
writable = parent_entry&.write_permission.present? && context.permitted?(parent_entry.write_permission)
|
|
262
|
+
if attribute && writable
|
|
263
|
+
sentences << "Write them in the #{attribute} attribute of #{parent} — describe_resource #{parent} shows the shape and the accepted types."
|
|
264
|
+
end
|
|
265
|
+
if attribute && readable && parent_entry.expandable_associations.include?(attribute)
|
|
266
|
+
sentences << "Read them with get_resource on #{parent} and expand: [\"#{attribute}\"]."
|
|
267
|
+
elsif readable && !attribute
|
|
268
|
+
sentences << "Start from #{parent}: get_resource with expand lists what it carries (describe_resource #{parent} names the options)."
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
{ error: sentences.join(' '), parent: parent }
|
|
272
|
+
end
|
|
273
|
+
|
|
170
274
|
# Refuses unless this admin may take `action` on `record`.
|
|
171
275
|
#
|
|
172
276
|
# Holding `write_products` says an admin may edit products; it does not
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
module AgentTools
|
|
3
|
-
# The tools Spree ships: the generic
|
|
4
|
-
#
|
|
3
|
+
# The hand-written tools Spree ships: the generic reads and writes,
|
|
4
|
+
# reporting, imports, files and stock.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# that
|
|
9
|
-
# replacement's schema; and a permission, which must equal the write scope
|
|
10
|
-
# of the admin controller that invokes the same workflow — a contract spec
|
|
11
|
-
# in `spree_api` fails when the two disagree, or when a controller gains a
|
|
12
|
-
# workflow that is neither listed here nor excluded below.
|
|
6
|
+
# Back-office actions are not listed here. Every Admin API action is a
|
|
7
|
+
# tool built from its OpenAPI operation ({OperationCatalog}); an action
|
|
8
|
+
# that should not be one is marked in its rswag spec, with the reason.
|
|
13
9
|
module DefaultCatalog
|
|
14
10
|
# Read tools, and the writes that are not workflows.
|
|
15
11
|
TOOLS = %w[
|
|
@@ -33,15 +29,11 @@ module Spree
|
|
|
33
29
|
].freeze
|
|
34
30
|
|
|
35
31
|
|
|
36
|
-
#
|
|
32
|
+
# @deprecated The 6.0.0 workflow allowlist, kept only so a reference to
|
|
33
|
+
# it does not break through 6.0.x. Nothing reads it: Admin API
|
|
34
|
+
# operations are tools on their own. Removed in 6.1.
|
|
37
35
|
WORKFLOWS = {
|
|
38
36
|
# Catalog
|
|
39
|
-
#
|
|
40
|
-
# The three status moves below run in this process rather than through
|
|
41
|
-
# an endpoint, because the Admin API has none for them: `PATCH` writes
|
|
42
|
-
# the column and the bulk action uses `update_all`, so neither
|
|
43
|
-
# publishes the status event nor withdraws an open seller submission.
|
|
44
|
-
# They dispatch as soon as an admin status endpoint exists.
|
|
45
37
|
product_activate_workflow: 'write_products',
|
|
46
38
|
product_draft_workflow: 'write_products',
|
|
47
39
|
product_archive_workflow: 'write_products',
|
|
@@ -59,8 +51,10 @@ module Spree
|
|
|
59
51
|
|
|
60
52
|
# Orders. `restock_items` is hidden: it is deprecated and ignored, so
|
|
61
53
|
# offering it would invite a model to set something that does nothing.
|
|
54
|
+
# `validated` is for callers that already ran their own veto; offered
|
|
55
|
+
# to a model it would be a way around orders.complete.validate.
|
|
62
56
|
order_cancel_workflow: { permission: 'write_orders', except: %i[restock_items] },
|
|
63
|
-
order_complete_workflow: 'write_orders',
|
|
57
|
+
order_complete_workflow: { permission: 'write_orders', except: %i[validated] },
|
|
64
58
|
|
|
65
59
|
# Fulfillment
|
|
66
60
|
fulfillment_create_workflow: 'write_fulfillments',
|
|
@@ -103,9 +97,9 @@ module Spree
|
|
|
103
97
|
tax_exemption_certificate_verify_workflow: 'write_customers'
|
|
104
98
|
}.freeze
|
|
105
99
|
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
#
|
|
100
|
+
# @deprecated The 6.0.0 exclusion list, kept like {WORKFLOWS}. Its
|
|
101
|
+
# reasons now travel as `agent_tool false, reason:` on the operation's
|
|
102
|
+
# rswag spec. Removed in 6.1.
|
|
109
103
|
# The email template editor is a draft-and-publish surface: its steps
|
|
110
104
|
# move a draft through revisions against a live preview, and they mean
|
|
111
105
|
# nothing outside that screen. Publishing one sends real mail in the
|
|
@@ -172,9 +166,10 @@ module Spree
|
|
|
172
166
|
# @return [void]
|
|
173
167
|
def install(registry)
|
|
174
168
|
TOOLS.each { |tool| registry << tool }
|
|
175
|
-
registry.expose_workflows(**WORKFLOWS)
|
|
176
169
|
end
|
|
177
170
|
end
|
|
171
|
+
|
|
172
|
+
deprecate_constant :WORKFLOWS, :EXCLUDED_WORKFLOWS
|
|
178
173
|
end
|
|
179
174
|
end
|
|
180
175
|
end
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module AgentTools
|
|
3
|
+
# The workflow tools Spree 6.0.0 offered, by the name and parameters a
|
|
4
|
+
# client saw then, and the operation each now runs.
|
|
5
|
+
#
|
|
6
|
+
# 6.0.0 derived a tool from a workflow's `perform` signature, so its
|
|
7
|
+
# parameters were the workflow's keywords (`order`, `reason`); a tool now
|
|
8
|
+
# takes the operation's own (`id`, `cancel_reason_id`). This table, written
|
|
9
|
+
# once from the 6.0.0 schemas, keeps those calls working through 6.0.x. It
|
|
10
|
+
# is an explicit list, not a rule: nothing here guesses one name from
|
|
11
|
+
# another.
|
|
12
|
+
#
|
|
13
|
+
# A parameter mapped to nil never reached the endpoint in 6.0.0 either; it
|
|
14
|
+
# is ignored, and the result says so. One mapped to `:merge` was a hash of
|
|
15
|
+
# fields, merged into the body, which is what 6.0.0 did with it.
|
|
16
|
+
#
|
|
17
|
+
# `deliveries_update_tracking` is not here: 6.0.0 sent it to the
|
|
18
|
+
# mark-delivered endpoint and dropped its tracking fields, so a call meant
|
|
19
|
+
# to record tracking marked the delivery delivered. No endpoint sets a
|
|
20
|
+
# carrier status, so the name no longer answers rather than doing that.
|
|
21
|
+
#
|
|
22
|
+
# Removed in 6.1, with the names that are no longer listed.
|
|
23
|
+
# See docs/plans/6.0-agent-tools-from-openapi.md.
|
|
24
|
+
module LegacyTools
|
|
25
|
+
# Old tool name => the operation it runs and its renamed parameters.
|
|
26
|
+
TOOLS = {
|
|
27
|
+
'catalogs_activate' => {
|
|
28
|
+
verb: 'patch', path: '/api/v3/admin/catalogs/{id}/activate',
|
|
29
|
+
parameters: { 'catalog' => 'id' }
|
|
30
|
+
},
|
|
31
|
+
'catalogs_deactivate' => {
|
|
32
|
+
verb: 'patch', path: '/api/v3/admin/catalogs/{id}/deactivate',
|
|
33
|
+
parameters: { 'catalog' => 'id' }
|
|
34
|
+
},
|
|
35
|
+
'fulfillments_create' => {
|
|
36
|
+
verb: 'post', path: '/api/v3/admin/orders/{order_id}/fulfillments',
|
|
37
|
+
parameters: {
|
|
38
|
+
'order' => 'order_id', 'stock_location' => 'stock_location_id',
|
|
39
|
+
'source_fulfillment' => 'source_fulfillment_id', 'delivery_method' => 'delivery_method_id'
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
'fulfillments_mark_delivered' => {
|
|
43
|
+
verb: 'patch', path: '/api/v3/admin/orders/{order_id}/fulfillments/{id}/mark_delivered',
|
|
44
|
+
parameters: { 'fulfillment' => 'id' }
|
|
45
|
+
},
|
|
46
|
+
'orders_cancel' => {
|
|
47
|
+
verb: 'patch', path: '/api/v3/admin/orders/{id}/cancel',
|
|
48
|
+
parameters: { 'order' => 'id', 'canceled_at' => nil, 'reason' => 'cancel_reason_id', 'note' => 'cancel_note' }
|
|
49
|
+
},
|
|
50
|
+
'orders_complete' => {
|
|
51
|
+
verb: 'patch', path: '/api/v3/admin/orders/{id}/complete',
|
|
52
|
+
parameters: { 'order' => 'id' }
|
|
53
|
+
},
|
|
54
|
+
'payments_capture' => {
|
|
55
|
+
verb: 'patch', path: '/api/v3/admin/orders/{order_id}/payments/{id}/capture',
|
|
56
|
+
parameters: { 'payment' => 'id' }
|
|
57
|
+
},
|
|
58
|
+
'payments_void' => {
|
|
59
|
+
verb: 'patch', path: '/api/v3/admin/orders/{order_id}/payments/{id}/void',
|
|
60
|
+
parameters: { 'payment' => 'id' }
|
|
61
|
+
},
|
|
62
|
+
'price_lists_activate' => {
|
|
63
|
+
verb: 'patch', path: '/api/v3/admin/price_lists/{id}/activate',
|
|
64
|
+
parameters: { 'price_list' => 'id' }
|
|
65
|
+
},
|
|
66
|
+
'price_lists_deactivate' => {
|
|
67
|
+
verb: 'patch', path: '/api/v3/admin/price_lists/{id}/deactivate',
|
|
68
|
+
parameters: { 'price_list' => 'id' }
|
|
69
|
+
},
|
|
70
|
+
'products_activate' => {
|
|
71
|
+
verb: 'patch', path: '/api/v3/admin/products/{id}/activate',
|
|
72
|
+
parameters: { 'product' => 'id' }
|
|
73
|
+
},
|
|
74
|
+
'products_approve' => {
|
|
75
|
+
verb: 'patch', path: '/api/v3/admin/products/{id}/approve',
|
|
76
|
+
parameters: { 'product' => 'id' }
|
|
77
|
+
},
|
|
78
|
+
'products_archive' => {
|
|
79
|
+
verb: 'patch', path: '/api/v3/admin/products/{id}/archive',
|
|
80
|
+
parameters: { 'product' => 'id' }
|
|
81
|
+
},
|
|
82
|
+
'products_draft' => {
|
|
83
|
+
verb: 'patch', path: '/api/v3/admin/products/{id}/draft',
|
|
84
|
+
parameters: { 'product' => 'id' }
|
|
85
|
+
},
|
|
86
|
+
'products_reject' => {
|
|
87
|
+
verb: 'patch', path: '/api/v3/admin/products/{id}/reject',
|
|
88
|
+
parameters: { 'product' => 'id' }
|
|
89
|
+
},
|
|
90
|
+
'purchase_orders_cancel' => {
|
|
91
|
+
verb: 'patch', path: '/api/v3/admin/purchase_orders/{id}/cancel',
|
|
92
|
+
parameters: { 'purchase_order' => 'id' }
|
|
93
|
+
},
|
|
94
|
+
'purchase_orders_close' => {
|
|
95
|
+
verb: 'patch', path: '/api/v3/admin/purchase_orders/{id}/close',
|
|
96
|
+
parameters: { 'purchase_order' => 'id' }
|
|
97
|
+
},
|
|
98
|
+
'purchase_orders_mark_draft' => {
|
|
99
|
+
verb: 'patch', path: '/api/v3/admin/purchase_orders/{id}/mark_draft',
|
|
100
|
+
parameters: { 'purchase_order' => 'id' }
|
|
101
|
+
},
|
|
102
|
+
'purchase_orders_mark_ordered' => {
|
|
103
|
+
verb: 'patch', path: '/api/v3/admin/purchase_orders/{id}/mark_ordered',
|
|
104
|
+
parameters: { 'purchase_order' => 'id' }
|
|
105
|
+
},
|
|
106
|
+
'purchase_orders_receive' => {
|
|
107
|
+
verb: 'post', path: '/api/v3/admin/purchase_orders/{purchase_order_id}/stock_receipts',
|
|
108
|
+
parameters: { 'purchase_order' => 'purchase_order_id' }
|
|
109
|
+
},
|
|
110
|
+
'refunds_create' => {
|
|
111
|
+
verb: 'post', path: '/api/v3/admin/orders/{order_id}/refunds',
|
|
112
|
+
parameters: {
|
|
113
|
+
'payment' => 'payment_id', 'reason' => 'refund_reason_id', 'originator' => nil,
|
|
114
|
+
'order' => 'order_id', 'tax_amount' => nil
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
'seller_requirement_submissions_accept' => {
|
|
118
|
+
verb: 'patch', path: '/api/v3/admin/sellers/{seller_id}/requirement_submissions/{id}/accept',
|
|
119
|
+
parameters: { 'submission' => 'id' }
|
|
120
|
+
},
|
|
121
|
+
'seller_requirement_submissions_reject' => {
|
|
122
|
+
verb: 'patch', path: '/api/v3/admin/sellers/{seller_id}/requirement_submissions/{id}/reject',
|
|
123
|
+
parameters: { 'submission' => 'id' }
|
|
124
|
+
},
|
|
125
|
+
'seller_requirement_submissions_waive' => {
|
|
126
|
+
verb: 'post', path: '/api/v3/admin/sellers/{seller_id}/requirement_submissions',
|
|
127
|
+
parameters: { 'seller' => 'seller_id', 'requirement' => 'requirement_id' }
|
|
128
|
+
},
|
|
129
|
+
'sellers_approve' => {
|
|
130
|
+
verb: 'patch', path: '/api/v3/admin/sellers/{id}/approve',
|
|
131
|
+
parameters: { 'seller' => 'id' }
|
|
132
|
+
},
|
|
133
|
+
'sellers_invite' => {
|
|
134
|
+
verb: 'post', path: '/api/v3/admin/sellers/{id}/invite',
|
|
135
|
+
parameters: { 'seller' => 'id', 'inviter' => nil, 'role' => 'role_id' }
|
|
136
|
+
},
|
|
137
|
+
'sellers_reject' => {
|
|
138
|
+
verb: 'patch', path: '/api/v3/admin/sellers/{id}/reject',
|
|
139
|
+
parameters: { 'seller' => 'id' }
|
|
140
|
+
},
|
|
141
|
+
'sellers_reopen_onboarding' => {
|
|
142
|
+
verb: 'patch', path: '/api/v3/admin/sellers/{id}/reopen_onboarding',
|
|
143
|
+
parameters: { 'seller' => 'id' }
|
|
144
|
+
},
|
|
145
|
+
'sellers_suspend' => {
|
|
146
|
+
verb: 'patch', path: '/api/v3/admin/sellers/{id}/suspend',
|
|
147
|
+
parameters: { 'seller' => 'id' }
|
|
148
|
+
},
|
|
149
|
+
'shipping_labels_purchase' => {
|
|
150
|
+
verb: 'post', path: '/api/v3/admin/orders/{order_id}/fulfillments/{fulfillment_id}/labels',
|
|
151
|
+
parameters: { 'owner' => 'fulfillment_id' }
|
|
152
|
+
},
|
|
153
|
+
'shipping_labels_refund' => {
|
|
154
|
+
verb: 'patch', path: '/api/v3/admin/orders/{order_id}/fulfillments/{fulfillment_id}/labels/{id}/refund',
|
|
155
|
+
parameters: { 'shipping_label' => 'id' }
|
|
156
|
+
},
|
|
157
|
+
'stock_transfers_cancel' => {
|
|
158
|
+
verb: 'patch', path: '/api/v3/admin/stock_transfers/{id}/cancel',
|
|
159
|
+
parameters: { 'stock_transfer' => 'id' }
|
|
160
|
+
},
|
|
161
|
+
'stock_transfers_close' => {
|
|
162
|
+
verb: 'patch', path: '/api/v3/admin/stock_transfers/{id}/close',
|
|
163
|
+
parameters: { 'stock_transfer' => 'id' }
|
|
164
|
+
},
|
|
165
|
+
'stock_transfers_mark_draft' => {
|
|
166
|
+
verb: 'patch', path: '/api/v3/admin/stock_transfers/{id}/mark_draft',
|
|
167
|
+
parameters: { 'stock_transfer' => 'id' }
|
|
168
|
+
},
|
|
169
|
+
'stock_transfers_mark_in_transit' => {
|
|
170
|
+
verb: 'patch', path: '/api/v3/admin/stock_transfers/{id}/mark_in_transit',
|
|
171
|
+
parameters: { 'stock_transfer' => 'id' }
|
|
172
|
+
},
|
|
173
|
+
'stock_transfers_mark_ready' => {
|
|
174
|
+
verb: 'patch', path: '/api/v3/admin/stock_transfers/{id}/mark_ready',
|
|
175
|
+
parameters: { 'stock_transfer' => 'id' }
|
|
176
|
+
},
|
|
177
|
+
'stock_transfers_receive' => {
|
|
178
|
+
verb: 'post', path: '/api/v3/admin/stock_transfers/{stock_transfer_id}/stock_receipts',
|
|
179
|
+
parameters: { 'stock_transfer' => 'stock_transfer_id' }
|
|
180
|
+
},
|
|
181
|
+
'tax_exemption_certificates_verify' => {
|
|
182
|
+
verb: 'patch', path: '/api/v3/admin/companies/{company_id}/tax_exemption_certificates/{id}/verify',
|
|
183
|
+
parameters: { 'certificate' => 'id' }
|
|
184
|
+
},
|
|
185
|
+
'variants_create' => {
|
|
186
|
+
verb: 'post', path: '/api/v3/admin/products/{product_id}/variants',
|
|
187
|
+
parameters: { 'product' => 'product_id', 'attributes' => :merge }
|
|
188
|
+
},
|
|
189
|
+
'variants_update' => {
|
|
190
|
+
verb: 'patch', path: '/api/v3/admin/products/{product_id}/variants/{id}',
|
|
191
|
+
parameters: { 'variant' => 'id', 'attributes' => :merge }
|
|
192
|
+
}
|
|
193
|
+
}.freeze
|
|
194
|
+
|
|
195
|
+
class << self
|
|
196
|
+
# The 6.0.0 parameters of a tool that keeps its name, when it still
|
|
197
|
+
# runs the same operation.
|
|
198
|
+
#
|
|
199
|
+
# @param name [String]
|
|
200
|
+
# @param verb [String]
|
|
201
|
+
# @param path [String] the OpenAPI path
|
|
202
|
+
# @return [Hash{String => String, Symbol, nil}]
|
|
203
|
+
def parameters_for(name, verb, path)
|
|
204
|
+
tool = TOOLS[name.to_s]
|
|
205
|
+
return {} unless tool && tool[:verb] == verb.to_s && tool[:path] == path
|
|
206
|
+
|
|
207
|
+
tool[:parameters]
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module AgentTools
|
|
3
|
+
# Every Admin API operation an agent may call by name, and what it takes.
|
|
4
|
+
#
|
|
5
|
+
# A contract like {ResourceMap}: core declares the shape, and `spree_api`
|
|
6
|
+
# fills it from the Admin API's OpenAPI document — core's and every
|
|
7
|
+
# extension's — and the routes behind it. An operation that gains an
|
|
8
|
+
# endpoint is a tool in the same commit, with the summary, description and
|
|
9
|
+
# request body its rswag spec wrote.
|
|
10
|
+
#
|
|
11
|
+
# See docs/plans/6.0-agent-tools-from-openapi.md.
|
|
12
|
+
module OperationCatalog
|
|
13
|
+
# @!attribute name
|
|
14
|
+
# @return [String] the tool name, `orders_cancel`
|
|
15
|
+
# @!attribute verb
|
|
16
|
+
# @return [String] 'post', 'patch', 'put', 'delete' or 'get'
|
|
17
|
+
# @!attribute path
|
|
18
|
+
# @return [String] the OpenAPI path, with `{name}` segments
|
|
19
|
+
# @!attribute description
|
|
20
|
+
# @return [String] what the model reads when choosing the tool
|
|
21
|
+
# @!attribute summary
|
|
22
|
+
# @return [String] the operation summary, for the confirmation line
|
|
23
|
+
# @!attribute permission
|
|
24
|
+
# @return [String] the scope gating it, from the routed controller
|
|
25
|
+
# @!attribute resource_key
|
|
26
|
+
# @return [String, nil] the resource-map key of the routed controller's
|
|
27
|
+
# model, so a parent segment can be read off the record
|
|
28
|
+
# @!attribute path_params
|
|
29
|
+
# @return [Array<String>] the dynamic segments, in path order
|
|
30
|
+
# @!attribute body_properties
|
|
31
|
+
# @return [Hash{String => Hash}] the request body's JSON Schema
|
|
32
|
+
# properties, references inlined
|
|
33
|
+
# @!attribute body_required
|
|
34
|
+
# @return [Array<String>]
|
|
35
|
+
# @!attribute path_descriptions
|
|
36
|
+
# @return [Hash{String => String}]
|
|
37
|
+
# @!attribute listed
|
|
38
|
+
# @return [Boolean] false for a 6.0.0 name kept callable through 6.0.x
|
|
39
|
+
# @!attribute legacy_parameters
|
|
40
|
+
# @return [Hash{String => String, nil}] 6.0.0 parameter => its name now
|
|
41
|
+
Entry = Struct.new(:name, :verb, :path, :description, :summary, :permission, :resource_key,
|
|
42
|
+
:path_params, :body_properties, :body_required, :path_descriptions,
|
|
43
|
+
:listed, :legacy_parameters, keyword_init: true) do
|
|
44
|
+
# @return [Boolean] a read opted in to the catalog
|
|
45
|
+
def read?
|
|
46
|
+
verb == 'get'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [Boolean] whether the call changes the store
|
|
50
|
+
def mutating
|
|
51
|
+
!read?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The resource each `{x_id}` segment names, read off the path itself —
|
|
55
|
+
# `/orders/{order_id}/items` names `orders` — so a renamed nested
|
|
56
|
+
# route still points at the right resource.
|
|
57
|
+
#
|
|
58
|
+
# @return [Hash{String => String}] segment => resource key
|
|
59
|
+
def segment_keys
|
|
60
|
+
path.scan(%r{/([a-z_]+)/\{([a-z_]+_id)\}}).to_h { |key, segment| [segment, key] }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The segments a call may leave out, read off the record a later
|
|
64
|
+
# segment names. The innermost is the record the call is about, never
|
|
65
|
+
# one to read off another.
|
|
66
|
+
#
|
|
67
|
+
# @return [Hash{String => String}] segment => resource key
|
|
68
|
+
def parent_keys
|
|
69
|
+
segment_keys.except(path_params.last)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @param values [Hash{String => String}] path segment => value
|
|
73
|
+
# @return [String, nil] nil while a segment is unfilled
|
|
74
|
+
def path_for(values)
|
|
75
|
+
filled = path.gsub(/\{([a-z_]+)\}/) do
|
|
76
|
+
value = values[Regexp.last_match(1)]
|
|
77
|
+
value.present? ? ERB::Util.url_encode(value.to_s) : Regexp.last_match(0)
|
|
78
|
+
end
|
|
79
|
+
filled unless filled.include?('{')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The tool's arguments as JSON Schema: the path segments and the
|
|
83
|
+
# request body, side by side.
|
|
84
|
+
#
|
|
85
|
+
# A segment or field a 6.0.0 name still reaches is left out of
|
|
86
|
+
# `required` through 6.0.x — a client validating against the schema
|
|
87
|
+
# would otherwise refuse the old name before the tool could map it —
|
|
88
|
+
# and so is a parent segment, which the tool can read off the record
|
|
89
|
+
# the call names. The tool checks both itself.
|
|
90
|
+
#
|
|
91
|
+
# @return [Hash]
|
|
92
|
+
def input_schema
|
|
93
|
+
properties = path_params.to_h do |segment|
|
|
94
|
+
[segment, { 'type' => 'string', 'description' => path_descriptions[segment] }.compact]
|
|
95
|
+
end.merge(body_properties)
|
|
96
|
+
|
|
97
|
+
aliased = legacy_parameters.values.grep(String)
|
|
98
|
+
parents = parent_keys.keys
|
|
99
|
+
# A 6.0.0 name no longer listed keeps the 6.0.0 contract, which
|
|
100
|
+
# required nothing the operation's body now does.
|
|
101
|
+
required = listed ? (path_params + body_required).uniq - aliased - parents : []
|
|
102
|
+
|
|
103
|
+
{ 'type' => 'object', 'properties' => properties, 'required' => required }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
class << self
|
|
108
|
+
# How the catalog fills itself the first time it is read. `spree_api`
|
|
109
|
+
# sets it to its walk over the document; an installation without the
|
|
110
|
+
# API engine has no operations to offer.
|
|
111
|
+
#
|
|
112
|
+
# @return [#call, nil]
|
|
113
|
+
attr_accessor :deriver
|
|
114
|
+
|
|
115
|
+
# @return [Array<Entry>] the operations offered by name
|
|
116
|
+
def all
|
|
117
|
+
derive!
|
|
118
|
+
entries.values
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The 6.0.0 names kept callable through 6.0.x, which no listing shows.
|
|
122
|
+
#
|
|
123
|
+
# @return [Array<Entry>]
|
|
124
|
+
def aliases
|
|
125
|
+
derive!
|
|
126
|
+
alias_entries.values
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# A tool by its name, including a 6.0.0 name no longer listed.
|
|
130
|
+
#
|
|
131
|
+
# @param name [String]
|
|
132
|
+
# @return [Entry, nil]
|
|
133
|
+
def find(name)
|
|
134
|
+
derive!
|
|
135
|
+
entries[name.to_s] || alias_entries[name.to_s]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Registers one operation. Re-registering a name replaces it, because
|
|
139
|
+
# the walk runs again after a code reload.
|
|
140
|
+
#
|
|
141
|
+
# @return [Entry]
|
|
142
|
+
def register(**attributes)
|
|
143
|
+
entry = build(listed: true, **attributes)
|
|
144
|
+
entries[entry.name] = entry
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Registers a 6.0.0 name for an operation, callable but not listed.
|
|
148
|
+
# A name an offered operation already has keeps that operation.
|
|
149
|
+
#
|
|
150
|
+
# @return [Entry, nil]
|
|
151
|
+
def register_alias(**attributes)
|
|
152
|
+
entry = build(**attributes, listed: false)
|
|
153
|
+
alias_entries[entry.name] = entry unless entries.key?(entry.name)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# @return [void]
|
|
157
|
+
def reset!
|
|
158
|
+
@entries = {}
|
|
159
|
+
@alias_entries = {}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Marks the catalog for rebuilding on its next read.
|
|
163
|
+
#
|
|
164
|
+
# @return [void]
|
|
165
|
+
def stale!
|
|
166
|
+
@derived = false
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Fills the catalog once, under a mutex. The flag is set only once the
|
|
170
|
+
# deriver has finished, so a second reader waits on the mutex instead
|
|
171
|
+
# of reading a catalog the deriver has just emptied. Nothing the
|
|
172
|
+
# deriver calls reads the catalog back, so it cannot recurse.
|
|
173
|
+
#
|
|
174
|
+
# @return [void]
|
|
175
|
+
def derive!
|
|
176
|
+
return if @derived || deriver.nil?
|
|
177
|
+
|
|
178
|
+
derive_mutex.synchronize do
|
|
179
|
+
next if @derived
|
|
180
|
+
|
|
181
|
+
deriver.call
|
|
182
|
+
@derived = true
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
private
|
|
187
|
+
|
|
188
|
+
def build(**attributes)
|
|
189
|
+
Entry.new(legacy_parameters: {}, body_properties: {}, body_required: [], path_descriptions: {}, **attributes)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def entries
|
|
193
|
+
@entries ||= {}
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def alias_entries
|
|
197
|
+
@alias_entries ||= {}
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def derive_mutex
|
|
201
|
+
@derive_mutex ||= Mutex.new
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|