spree_api 6.0.0.rc4 → 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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/spree/api/v3/allowed_redirect_url.rb +27 -0
  3. data/app/controllers/concerns/spree/api/v3/cart_resolvable.rb +8 -2
  4. data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +6 -2
  5. data/app/controllers/spree/api/v3/admin/companies/addresses_controller.rb +7 -1
  6. data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +30 -8
  7. data/app/controllers/spree/api/v3/admin/customers/addresses_controller.rb +4 -0
  8. data/app/controllers/spree/api/v3/admin/customers_controller.rb +1 -6
  9. data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +1 -8
  10. data/app/controllers/spree/api/v3/admin/orders_controller.rb +11 -5
  11. data/app/controllers/spree/api/v3/admin/package_types_controller.rb +2 -3
  12. data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +3 -4
  13. data/app/controllers/spree/api/v3/admin/products_controller.rb +5 -3
  14. data/app/controllers/spree/api/v3/admin/store_controller.rb +1 -1
  15. data/app/controllers/spree/api/v3/admin/variants_controller.rb +96 -1
  16. data/app/controllers/spree/api/v3/oauth/authorizations_controller.rb +17 -6
  17. data/app/controllers/spree/api/v3/resource_controller.rb +35 -5
  18. data/app/controllers/spree/api/v3/seller/package_types_controller.rb +2 -2
  19. data/app/controllers/spree/api/v3/seller/products_controller.rb +7 -61
  20. data/app/controllers/spree/api/v3/seller/resource_controller.rb +2 -6
  21. data/app/controllers/spree/api/v3/store/carts/addresses_controller.rb +53 -0
  22. data/app/controllers/spree/api/v3/store/carts_controller.rb +3 -3
  23. data/app/controllers/spree/api/v3/store/companies/addresses_controller.rb +4 -0
  24. data/app/controllers/spree/api/v3/store/customer/addresses_controller.rb +4 -0
  25. data/app/controllers/spree/api/v3/store/customer/data_requests_controller.rb +19 -1
  26. data/app/controllers/spree/api/v3/store/customer/password_resets_controller.rb +3 -14
  27. data/app/controllers/spree/api/v3/store/data_request_confirmations_controller.rb +23 -0
  28. data/app/controllers/spree/api/v3/store/data_requests_controller.rb +56 -0
  29. data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +2 -11
  30. data/app/request_schemas/spree/api/v3/admin/variant_request_schema.rb +21 -0
  31. data/app/request_schemas/spree/api/v3/seller/price_request_schema.rb +20 -0
  32. data/app/request_schemas/spree/api/v3/seller/variant_request_schema.rb +19 -0
  33. data/app/request_schemas/spree/api/v3/variant_request_schema.rb +8 -13
  34. data/app/serializers/spree/api/v3/address_owner_serializer.rb +27 -0
  35. data/app/serializers/spree/api/v3/address_serializer.rb +5 -1
  36. data/app/serializers/spree/api/v3/admin/address_owner_serializer.rb +10 -0
  37. data/app/serializers/spree/api/v3/admin/address_serializer.rb +1 -10
  38. data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +1 -1
  39. data/app/serializers/spree/api/v3/admin/catalog_assignment_serializer.rb +1 -1
  40. data/app/serializers/spree/api/v3/admin/catalog_price_serializer.rb +1 -1
  41. data/app/serializers/spree/api/v3/admin/collection_rule_serializer.rb +1 -1
  42. data/app/serializers/spree/api/v3/admin/collection_serializer.rb +1 -1
  43. data/app/serializers/spree/api/v3/admin/commission_line_serializer.rb +2 -2
  44. data/app/serializers/spree/api/v3/admin/commission_rate_serializer.rb +1 -1
  45. data/app/serializers/spree/api/v3/admin/consent_record_serializer.rb +1 -1
  46. data/app/serializers/spree/api/v3/admin/email_template_serializer.rb +1 -1
  47. data/app/serializers/spree/api/v3/admin/order_serializer.rb +1 -1
  48. data/app/serializers/spree/api/v3/admin/package_type_serializer.rb +3 -3
  49. data/app/serializers/spree/api/v3/admin/permission_serializer.rb +1 -1
  50. data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +1 -1
  51. data/app/serializers/spree/api/v3/admin/purchase_order_serializer.rb +1 -1
  52. data/app/serializers/spree/api/v3/admin/seller_requirement_status_serializer.rb +1 -1
  53. data/app/serializers/spree/api/v3/admin/seller_serializer.rb +2 -2
  54. data/app/serializers/spree/api/v3/admin/seller_transfer_serializer.rb +1 -1
  55. data/app/serializers/spree/api/v3/admin/shipping_label_serializer.rb +3 -3
  56. data/app/serializers/spree/api/v3/admin/stock_location_serializer.rb +1 -1
  57. data/app/serializers/spree/api/v3/admin/stock_receipt_item_serializer.rb +2 -2
  58. data/app/serializers/spree/api/v3/admin/stock_receipt_serializer.rb +1 -1
  59. data/app/serializers/spree/api/v3/admin/stock_transfer_serializer.rb +1 -1
  60. data/app/serializers/spree/api/v3/admin/store_serializer.rb +1 -0
  61. data/app/serializers/spree/api/v3/admin/tax_identifier_serializer.rb +2 -2
  62. data/app/serializers/spree/api/v3/admin/tax_line_serializer.rb +1 -1
  63. data/app/serializers/spree/api/v3/admin/variant_serializer.rb +1 -1
  64. data/app/serializers/spree/api/v3/base_serializer.rb +27 -5
  65. data/app/serializers/spree/api/v3/claim_serializer.rb +1 -1
  66. data/app/serializers/spree/api/v3/collection_serializer.rb +1 -1
  67. data/app/serializers/spree/api/v3/company_invitation_serializer.rb +1 -1
  68. data/app/serializers/spree/api/v3/company_serializer.rb +1 -1
  69. data/app/serializers/spree/api/v3/data_request_confirmation_requested_event_serializer.rb +28 -0
  70. data/app/serializers/spree/api/v3/data_request_event_serializer.rb +1 -1
  71. data/app/serializers/spree/api/v3/data_request_serializer.rb +8 -4
  72. data/app/serializers/spree/api/v3/delivery_serializer.rb +1 -1
  73. data/app/serializers/spree/api/v3/delivery_zone_member_serializer.rb +1 -1
  74. data/app/serializers/spree/api/v3/discount_serializer.rb +1 -1
  75. data/app/serializers/spree/api/v3/file_upload_serializer.rb +1 -1
  76. data/app/serializers/spree/api/v3/fulfillment_serializer.rb +1 -1
  77. data/app/serializers/spree/api/v3/media_event_serializer.rb +1 -1
  78. data/app/serializers/spree/api/v3/media_serializer.rb +1 -1
  79. data/app/serializers/spree/api/v3/option_type_serializer.rb +1 -1
  80. data/app/serializers/spree/api/v3/order_group_serializer.rb +2 -2
  81. data/app/serializers/spree/api/v3/order_serializer.rb +4 -2
  82. data/app/serializers/spree/api/v3/payment_serializer.rb +1 -1
  83. data/app/serializers/spree/api/v3/product_filter_availability_option_serializer.rb +1 -1
  84. data/app/serializers/spree/api/v3/product_filter_option_serializer.rb +1 -1
  85. data/app/serializers/spree/api/v3/seller/address_serializer.rb +3 -2
  86. data/app/serializers/spree/api/v3/seller/order_serializer.rb +3 -3
  87. data/app/serializers/spree/api/v3/seller/package_type_serializer.rb +3 -3
  88. data/app/serializers/spree/api/v3/seller/profile_serializer.rb +2 -2
  89. data/app/serializers/spree/api/v3/seller/requirement_status_serializer.rb +1 -1
  90. data/app/serializers/spree/api/v3/seller/tax_identifier_serializer.rb +1 -1
  91. data/app/serializers/spree/api/v3/seller/variant_serializer.rb +1 -1
  92. data/app/serializers/spree/api/v3/shipping_label_event_serializer.rb +1 -1
  93. data/app/serializers/spree/api/v3/stock_movement_serializer.rb +1 -1
  94. data/app/serializers/spree/api/v3/stock_receipt_event_serializer.rb +1 -1
  95. data/app/serializers/spree/api/v3/variant_serializer.rb +2 -2
  96. data/app/services/spree/api/v3/filter_table.rb +22 -3
  97. data/app/services/spree/api/v3/request_schema.rb +64 -17
  98. data/config/locales/en.yml +1 -0
  99. data/config/routes.rb +19 -11
  100. data/lib/spree/api/agent_write_schemas.rb +15 -3
  101. data/lib/spree/api/dependencies.rb +2 -2
  102. data/lib/spree/api/enum_types.rb +118 -0
  103. data/lib/spree/api/openapi/filter_documentation.rb +8 -1
  104. data/lib/spree/api/typelizer_generation.rb +5 -0
  105. data/lib/typelizer/open_enums.rb +40 -16
  106. metadata +17 -6
@@ -15,14 +15,14 @@ module Spree
15
15
  # refuses to write them (docs/plans/6.0-seller-package-types.md).
16
16
  class PackageTypeSerializer < V3::BaseSerializer
17
17
  typelize name: :string,
18
- kind: [:string, enum: Spree::PackageType::KINDS],
18
+ kind: [:string, enum: Spree::PackageType::KINDS, enum_type_name: 'PackageTypeKind'],
19
19
  length: [:string, nullable: true],
20
20
  width: [:string, nullable: true],
21
21
  height: [:string, nullable: true],
22
- dimensions_unit: [:string, enum: Spree::Variant::DIMENSION_UNITS],
22
+ dimensions_unit: [:string, enum: Spree::Variant::DIMENSION_UNITS, enum_type_name: 'DimensionUnit'],
23
23
  weight: [:string, nullable: true],
24
24
  max_weight: [:string, nullable: true],
25
- weight_unit: [:string, enum: Spree::Variant::WEIGHT_UNITS],
25
+ weight_unit: [:string, enum: Spree::Variant::WEIGHT_UNITS, enum_type_name: 'WeightUnit'],
26
26
  volume: [:string, nullable: true],
27
27
  default: :boolean,
28
28
  editable: :boolean,
@@ -8,8 +8,8 @@ module Spree
8
8
  registration_number: [:string, nullable: true],
9
9
  contact_email: [:string, nullable: true],
10
10
  billing_email: [:string, nullable: true],
11
- tax_remittance: :string,
12
- payouts_schedule_interval: [:string, nullable: true],
11
+ tax_remittance: [:string, enum: Spree::Seller::TAX_REMITTANCES, enum_type_name: 'TaxRemittance'],
12
+ payouts_schedule_interval: [:string, nullable: true, enum: Spree::Seller::PAYOUT_INTERVALS, enum_type_name: 'PayoutScheduleInterval'],
13
13
  minimum_payout_amount: [:string, nullable: true],
14
14
  holiday_mode_until: [:string, nullable: true],
15
15
  terms_accepted_at: [:string, nullable: true],
@@ -8,7 +8,7 @@ module Spree
8
8
 
9
9
  typelize id: :string, kind: [:string, comment: 'Requirement kind. Built-in: accept_terms, complete_profile, billing_address, returns_address, delivery_method, package_type, minimum_products, payout_account, required_custom_fields, policy, attestation, operator_review, document. Extensions may register more.'], name: :string,
10
10
  description: [:string, nullable: true],
11
- required: :boolean, position: :number, status: [:string, enum: Spree::SellerRequirementStatus::STATUSES],
11
+ required: :boolean, position: :number, status: [:string, enum: Spree::SellerRequirementStatus::STATUSES, enum_type_name: 'SellerRequirementState'],
12
12
  blocking: :boolean, action_url: [:string, nullable: true],
13
13
  blocker: ['{ state: string; message: string | null } | null'],
14
14
  accepts_submissions: :boolean, requires_file: :boolean,
@@ -4,7 +4,7 @@ module Spree
4
4
  module Seller
5
5
  class TaxIdentifierSerializer < V3::BaseSerializer
6
6
  typelize kind: [:string, comment: 'Tax identifier kind, keyed to a registered validator. Built-in: eu_vat. Extensions may register more.'], value: :string,
7
- validation_status: [:string, nullable: true, enum: Spree::TaxIdentifier::VALIDATION_STATUSES],
7
+ validation_status: [:string, nullable: true, enum: Spree::TaxIdentifier::VALIDATION_STATUSES, enum_type_name: 'TaxIdentifierValidationStatus'],
8
8
  validated_at: [:string, nullable: true]
9
9
 
10
10
  attributes :kind, :value, :validation_status, validated_at: :iso8601
@@ -25,7 +25,7 @@ module Spree
25
25
  country_of_origin: [:string, nullable: true, enum: Spree::IsoData.country_codes, enum_type_name: 'CountryCode'],
26
26
  customs_description: [:string, nullable: true],
27
27
  minimum_order_quantity: ['number | null'], order_multiple: ['number | null'],
28
- purchase_unit: [:string, nullable: true, enum: Spree::Variant::PURCHASE_UNITS],
28
+ purchase_unit: [:string, nullable: true, enum: Spree::Variant::PURCHASE_UNITS, enum_type_name: 'PurchaseUnit'],
29
29
  metadata: 'Record<string, unknown>'
30
30
 
31
31
  attributes :metadata, :position, :cost_currency,
@@ -6,7 +6,7 @@ module Spree
6
6
  # stay behind the Admin API.
7
7
  class ShippingLabelEventSerializer < BaseSerializer
8
8
  typelize owner_id: :string,
9
- owner_type: [:string, enum: %w[fulfillment return]],
9
+ owner_type: [:string, enum: %w[fulfillment return], enum_type_name: 'ShippingLabelOwnerType'],
10
10
  status: [:string, enum: Spree::ShippingLabel.statuses, enum_type_name: 'ShippingLabelStatus'],
11
11
  carrier: [:string, nullable: true],
12
12
  service: [:string, nullable: true],
@@ -7,7 +7,7 @@ module Spree
7
7
  # `kind` is nullable for the lifetime of 6.0: a legacy row on an
8
8
  # install that has not run spree:migrate_stock_movements_to_typed_rows
9
9
  # has no kind yet.
10
- typelize quantity: :number, kind: [:string, nullable: true, enum: Spree::StockMovement::KINDS],
10
+ typelize quantity: :number, kind: [:string, nullable: true, enum: Spree::StockMovement::KINDS, enum_type_name: 'StockMovementKind'],
11
11
  reason: [:string, nullable: true], stock_level_id: [:string, nullable: true]
12
12
 
13
13
  attributes :quantity, :kind, :reason,
@@ -6,7 +6,7 @@ module Spree
6
6
  class StockReceiptEventSerializer < BaseSerializer
7
7
  typelize number: :string,
8
8
  received_at: :string,
9
- receivable_type: [:string, enum: %w[purchase_order stock_transfer]],
9
+ receivable_type: [:string, enum: %w[purchase_order stock_transfer], enum_type_name: 'StockReceiptReceivableType'],
10
10
  receivable_id: :string,
11
11
  quantity_accepted_total: :number,
12
12
  quantity_rejected_total: :number
@@ -10,11 +10,11 @@ module Spree
10
10
  purchasable: :boolean, in_stock: :boolean, backorderable: :boolean, preorder: :boolean,
11
11
  preorder_ships_at: [:string, nullable: true],
12
12
  weight: [:number, nullable: true], height: [:number, nullable: true], width: [:number, nullable: true], depth: [:number, nullable: true],
13
- weight_unit: [:string, enum: Spree::Variant::WEIGHT_UNITS], dimensions_unit: [:string, enum: Spree::Variant::DIMENSION_UNITS],
13
+ weight_unit: [:string, enum: Spree::Variant::WEIGHT_UNITS, enum_type_name: 'WeightUnit'], dimensions_unit: [:string, enum: Spree::Variant::DIMENSION_UNITS, enum_type_name: 'DimensionUnit'],
14
14
  price: 'Price',
15
15
  original_price: ['Price', nullable: true],
16
16
  minimum_order_quantity: :number, order_multiple: :number,
17
- purchase_unit: [:string, enum: Spree::Variant::PURCHASE_UNITS], units_per_carton: ['number | null'],
17
+ purchase_unit: [:string, enum: Spree::Variant::PURCHASE_UNITS, enum_type_name: 'PurchaseUnit'], units_per_carton: ['number | null'],
18
18
  seller_id: [:string, nullable: true]
19
19
 
20
20
  prefixed_id_attributes :product
@@ -25,6 +25,17 @@ module Spree
25
25
  boolean: 'boolean'
26
26
  }.freeze
27
27
 
28
+ # Columns holding a value from one of the closed code lists the API
29
+ # publishes as a shared type, matched by name (`locale`,
30
+ # `default_locale`, `cost_currency`), so clients filter with that type,
31
+ # not free text.
32
+ CODE_TYPES = {
33
+ 'locale' => 'LocaleCode',
34
+ 'currency' => 'CurrencyCode',
35
+ 'country_code' => 'CountryCode',
36
+ 'country_of_origin' => 'CountryCode'
37
+ }.freeze
38
+
28
39
  # Raised when an endpoint's table cannot be described — a column of a
29
40
  # kind the contract has no predicates for, or two models sharing a name.
30
41
  class Error < StandardError; end
@@ -124,8 +135,9 @@ module Spree
124
135
  self.class.name_for(model)
125
136
  end
126
137
 
127
- # @return [Hash{String => String, Hash}] attribute => value kind, or
128
- # `{ 'enum' => [values] }` for a status
138
+ # @return [Hash{String => String, Hash}] attribute => value kind,
139
+ # `{ 'enum' => [values] }` for a status, or `{ 'code' => type }` for
140
+ # a value from a shared code list
129
141
  def attributes
130
142
  @attributes ||= kinds.reject { |_, kind| kind == :unclassified }.compact
131
143
  end
@@ -210,6 +222,10 @@ module Spree
210
222
  # attribute the model does not have (`position` on a table without
211
223
  # one), `:unclassified` for one the contract cannot describe
212
224
  def attribute_kind(attribute)
225
+ if (code_type = code_type(attribute))
226
+ return { 'code' => code_type }
227
+ end
228
+
213
229
  aliased = model._ransack_aliases[attribute] || model.attribute_aliases[attribute]
214
230
  return attribute_kind(aliased) if aliased
215
231
  return ransacker_kind(model._ransackers[attribute]) if model._ransackers.key?(attribute)
@@ -220,7 +236,6 @@ module Spree
220
236
  if (values = inclusion_values(attribute))
221
237
  return { 'enum' => values }
222
238
  end
223
-
224
239
  column = model.columns_hash[attribute]
225
240
  return COLUMN_KINDS.fetch(column.type, :unclassified) if column
226
241
  return 'text' if translated_attribute?(attribute)
@@ -229,6 +244,10 @@ module Spree
229
244
  :unclassified
230
245
  end
231
246
 
247
+ def code_type(attribute)
248
+ CODE_TYPES.find { |name, _| attribute == name || attribute.end_with?("_#{name}") }&.last
249
+ end
250
+
232
251
  # The fixed list an inclusion validation allows, for a column that
233
252
  # holds one of a known set of values (`payment_status`, `kind`).
234
253
  def inclusion_values(attribute)
@@ -40,20 +40,24 @@ module Spree
40
40
  datetime: { type: :string, format: :'date-time' },
41
41
  id: { type: :string },
42
42
  any: {},
43
- # ISO codes, from the closed lists below.
43
+ # Codes from the closed lists below.
44
44
  country: { type: :string },
45
45
  currency: { type: :string },
46
- locale: { type: :string }
46
+ locale: { type: :string },
47
+ weight_unit: { type: :string },
48
+ dimension_unit: { type: :string }
47
49
  }.freeze
48
50
 
49
- # The ISO lists a `country` or `currency` field takes, published once
50
- # per API as a named component — the same `CountryCode` and
51
- # `CurrencyCode` types the serializers declare, so a value read from
52
- # a response is the type a request takes.
51
+ # The closed lists a code field takes, published once per API as a
52
+ # named component — the same `CountryCode`, `CurrencyCode` and
53
+ # `WeightUnit` types the serializers declare, so a value read from a
54
+ # response is the type a request takes.
53
55
  CODE_LISTS = {
54
56
  country: ['CountryCode', -> { Spree::IsoData.country_codes }],
55
57
  currency: ['CurrencyCode', -> { Spree::Money::Rounding::CURRENCY_CODES }],
56
- locale: ['LocaleCode', -> { Spree::Locales::ALL }]
58
+ locale: ['LocaleCode', -> { Spree::Locales::ALL }],
59
+ weight_unit: ['WeightUnit', -> { Spree::Variant::WEIGHT_UNITS }],
60
+ dimension_unit: ['DimensionUnit', -> { Spree::Variant::DIMENSION_UNITS }]
57
61
  }.freeze
58
62
 
59
63
  ACTIONS = %i[create update].freeze
@@ -119,7 +123,7 @@ module Spree
119
123
  end
120
124
  end
121
125
 
122
- attr_reader :name, :fields
126
+ attr_reader :name, :fields, :base
123
127
 
124
128
  # The types an attribute takes: the scalars above, plus `:hash` (any
125
129
  # keys), `:object` (a block or a `shape:`) and `:array` (`of:` a type
@@ -172,7 +176,7 @@ module Spree
172
176
  value.is_a?(Class) ? value.definition : value
173
177
  end
174
178
 
175
- %i[attribute include_attributes require_attributes external_references].each do |method|
179
+ %i[attribute extends include_attributes require_attributes external_references].each do |method|
176
180
  define_method(method) { |*args, **options, &block| definition.public_send(method, *args, **options, &block) }
177
181
  end
178
182
  end
@@ -229,6 +233,19 @@ module Spree
229
233
  TEXT
230
234
  end
231
235
 
236
+ # Builds on a published shape: the body accepts all of its attributes,
237
+ # and the published type is that shape's type plus what this schema
238
+ # adds, so clients write one type wherever the shape is accepted.
239
+ #
240
+ # @param other [RequestSchema, Class] a shape with a published name
241
+ def extends(other)
242
+ parent = RequestSchema.resolve(other)
243
+ raise ArgumentError, "#{parent.class.name} has no published name to extend" unless shape?(parent)
244
+
245
+ include_attributes(parent)
246
+ @base = parent
247
+ end
248
+
232
249
  # Adds the attributes of another schema, for endpoints that extend a
233
250
  # shared set.
234
251
  #
@@ -253,6 +270,19 @@ module Spree
253
270
  @permit_lists = nil
254
271
  end
255
272
 
273
+ # Replaces the description of an attribute this schema took from
274
+ # another, for an endpoint where it behaves differently.
275
+ #
276
+ # @param name [Symbol]
277
+ # @param description [String]
278
+ # @return [void]
279
+ def describe_attribute(name, description)
280
+ field = fields.fetch(name) { raise ArgumentError, "#{name} is not an attribute of this schema" }
281
+ copy = field.dup
282
+ copy.instance_variable_set(:@description, description)
283
+ fields[name] = copy
284
+ end
285
+
256
286
  # The `params.permit` list for an action.
257
287
  #
258
288
  # @param action [Symbol, String, nil] `create` or `update`; nil
@@ -269,12 +299,21 @@ module Spree
269
299
  # @return [Hash]
270
300
  def json_schema(action = nil)
271
301
  action = action&.to_sym
272
- fields = fields_for(action)
273
- required = fields.select { |field| field.required_on?(action) }.map { |field| field.name.to_s }
302
+ # A base field this action does not take would still reach clients
303
+ # through the base's type, so such a body is published in full.
304
+ unless base && base.fields.each_value.all? { |field| field.accepted_on?(action) }
305
+ return object_schema(fields_for(action), action)
306
+ end
274
307
 
275
- schema = { type: :object, properties: fields.to_h { |field| [field.name, property_schema(field)] } }
276
- schema[:required] = required if required.any?
277
- schema
308
+ # Attributes the base describes the same way for this action are
309
+ # left to its type.
310
+ own = fields_for(action).reject do |field|
311
+ inherited = base.fields[field.name]
312
+ inherited && inherited.required_on?(action) == field.required_on?(action) &&
313
+ property_schema(inherited) == property_schema(field)
314
+ end
315
+ reference = { '$ref': "#/components/schemas/#{base.name}" }
316
+ own.any? ? { allOf: [reference, object_schema(own, action)] } : reference
278
317
  end
279
318
 
280
319
  # The component name published for an action.
@@ -291,15 +330,15 @@ module Spree
291
330
  #
292
331
  # @return [Array<RequestSchema>]
293
332
  def shapes
294
- fields.each_value.flat_map do |field|
333
+ (base ? [base, *base.shapes] : []).concat(fields.each_value.flat_map do |field|
295
334
  next [] unless field.schema
296
335
 
297
336
  nested = field.schema.shapes
298
337
  shape?(field.schema) ? [field.schema, *nested] : nested
299
- end.uniq
338
+ end).uniq
300
339
  end
301
340
 
302
- # The ISO code lists this schema's fields reach, nested ones included,
341
+ # The code lists this schema's fields reach, nested ones included,
303
342
  # as the components they publish.
304
343
  #
305
344
  # @return [Hash{String => Hash}]
@@ -374,6 +413,14 @@ module Spree
374
413
  end
375
414
 
376
415
 
416
+ def object_schema(fields, action)
417
+ required = fields.select { |field| field.required_on?(action) }.map { |field| field.name.to_s }
418
+
419
+ schema = { type: :object, properties: fields.to_h { |field| [field.name, property_schema(field)] } }
420
+ schema[:required] = required if required.any?
421
+ schema
422
+ end
423
+
377
424
  def fields_for(action)
378
425
  fields.values.select { |field| field.accepted_on?(action) }
379
426
  end
@@ -14,6 +14,7 @@ en:
14
14
  media_in_use: This file is still in use (%{places}). Pass detach=true to remove it everywhere and delete it, or clear those places first.
15
15
  media_not_deleted: The file could not be deleted. Nothing was changed.
16
16
  media_url_import_requires_product: Importing media from a URL needs a product to import it into. Upload the file instead, or post it to a product's media.
17
+ order_sales_context_exclusive: An order already fixes its sales context, so order_id cannot be combined with %{params}.
17
18
  store_mismatch: The requested store does not match the store this API key belongs to.
18
19
  storefront_login_required: Authentication required to access this store.
19
20
  unsupported_export_type: 'Unsupported export type. Supported types: %{types}'
data/config/routes.rb CHANGED
@@ -43,6 +43,7 @@ Spree::Core::Engine.add_routes do
43
43
  patch :associate
44
44
  post :complete
45
45
  end
46
+ resources :addresses, only: [:index], controller: 'carts/addresses'
46
47
  resources :items, only: [:create, :update, :destroy], controller: 'carts/items'
47
48
  resources :discount_codes, only: [:create, :destroy], controller: 'carts/discount_codes'
48
49
  resources :gift_cards, only: [:create, :destroy], controller: 'carts/gift_cards'
@@ -154,6 +155,10 @@ Spree::Core::Engine.add_routes do
154
155
  # Token-addressed so the emailed link works from a device that was
155
156
  # never signed in.
156
157
  get 'data_requests/:token/download', to: 'data_request_downloads#show', as: :data_request_download
158
+ # Addressed by the confirmation token emailed to the address a
159
+ # customer's own request names, for the same reason.
160
+ get 'data_requests/:token', to: 'data_requests#show', as: :data_request
161
+ post 'data_requests/:token/confirmation', to: 'data_request_confirmations#create', as: :data_request_confirmation
157
162
  # Legacy path — emailed and bookmarked URLs outlive the rename. Removed in 6.1.
158
163
  get 'digitals/:token', to: 'digital_links#show', as: :digital_download
159
164
 
@@ -282,6 +287,9 @@ Spree::Core::Engine.add_routes do
282
287
  resource :store, only: [:show, :update], controller: 'store' do
283
288
  get :data_sources
284
289
  end
290
+ # The store's own custom fields, addressed by its id like every other
291
+ # parent. The controller only ever resolves the current store.
292
+ resources :stores, only: [], concerns: :custom_fieldable
285
293
 
286
294
  # Staff & access (invitations, admin users, roles, API keys)
287
295
  resources :admin_users, only: [:index, :show, :update, :destroy]
@@ -345,7 +353,7 @@ Spree::Core::Engine.add_routes do
345
353
  end
346
354
 
347
355
  # Products
348
- resources :product_types do
356
+ resources :product_types, concerns: :custom_fieldable do
349
357
  post :apply_to_products, on: :member
350
358
  end
351
359
  resources :products, concerns: [:custom_fieldable, :translatable] do
@@ -387,7 +395,7 @@ Spree::Core::Engine.add_routes do
387
395
  # Media library — every file in the store, placed or not. Files are put
388
396
  # ON a product through the nested media routes above; this is where they
389
397
  # are uploaded, browsed and deleted.
390
- resources :media, controller: 'media_library', only: [:index, :show, :create, :update, :destroy] do
398
+ resources :media, controller: 'media_library', only: [:index, :show, :create, :update, :destroy], concerns: :custom_fieldable do
391
399
  get :usage, on: :member
392
400
  end
393
401
 
@@ -422,7 +430,7 @@ Spree::Core::Engine.add_routes do
422
430
  resources :tax_categories
423
431
 
424
432
  # Tax Rates — the internal tax provider's configuration
425
- resources :tax_rates
433
+ resources :tax_rates, concerns: :custom_fieldable
426
434
 
427
435
  # Selectable tax engines and their declared limits (discovery only)
428
436
  resources :tax_providers, only: [:index]
@@ -447,7 +455,7 @@ Spree::Core::Engine.add_routes do
447
455
  post :bulk_upsert, on: :collection
448
456
  end
449
457
  resources :stock_movements, only: [:index, :show]
450
- resources :stock_transfers do
458
+ resources :stock_transfers, concerns: :custom_fieldable do
451
459
  member do
452
460
  patch :mark_ready
453
461
  patch :mark_in_transit
@@ -460,7 +468,7 @@ Spree::Core::Engine.add_routes do
460
468
 
461
469
  # Purchasing
462
470
  resources :suppliers
463
- resources :purchase_orders do
471
+ resources :purchase_orders, concerns: :custom_fieldable do
464
472
  member do
465
473
  patch :mark_ordered
466
474
  patch :mark_draft
@@ -471,7 +479,7 @@ Spree::Core::Engine.add_routes do
471
479
  end
472
480
 
473
481
  # Payment Methods
474
- resources :delivery_methods do
482
+ resources :delivery_methods, concerns: :custom_fieldable do
475
483
  collection do
476
484
  get :calculators
477
485
  get :fulfillment_providers
@@ -492,7 +500,7 @@ Spree::Core::Engine.add_routes do
492
500
  only: [:index, :show, :create, :update, :destroy]
493
501
  end
494
502
 
495
- resources :payment_methods do
503
+ resources :payment_methods, concerns: :custom_fieldable do
496
504
  get :types, on: :collection
497
505
  end
498
506
 
@@ -502,7 +510,7 @@ Spree::Core::Engine.add_routes do
502
510
  end
503
511
 
504
512
  # Promotions, with nested actions/rules/coupon codes.
505
- resources :promotions do
513
+ resources :promotions, concerns: :custom_fieldable do
506
514
  resources :promotion_actions, only: [:index, :show, :create, :update, :destroy]
507
515
  resources :promotion_rules, only: [:index, :show, :create, :update, :destroy]
508
516
  resources :coupon_codes, only: [:index, :show]
@@ -635,7 +643,7 @@ Spree::Core::Engine.add_routes do
635
643
  # Business customers — a tree of company nodes. Nested collections are
636
644
  # listed and created under their node and then addressed directly, so
637
645
  # a caller holding an entry id does not have to know its company.
638
- resources :companies do
646
+ resources :companies, concerns: :custom_fieldable do
639
647
  # The node's address book.
640
648
  resources :addresses, controller: 'companies/addresses'
641
649
 
@@ -720,12 +728,12 @@ Spree::Core::Engine.add_routes do
720
728
  end
721
729
 
722
730
  # Gift cards
723
- resources :gift_cards
731
+ resources :gift_cards, concerns: :custom_fieldable
724
732
  resources :gift_card_batches, only: [:index, :show, :create]
725
733
 
726
734
  # Store credits across all customers. Read-only: issuing, editing and
727
735
  # deleting a credit stays nested under the customer that holds it.
728
- resources :store_credits, only: [:index, :show] do
736
+ resources :store_credits, only: [:index, :show], concerns: :custom_fieldable do
729
737
  resources :events, only: [:index], controller: 'store_credits/events'
730
738
  end
731
739
 
@@ -100,19 +100,31 @@ module Spree
100
100
 
101
101
  # A request body is declared once per endpoint and published as a
102
102
  # component, so the operation carries a `$ref` to it rather than the
103
- # properties themselves. Followed a few hops, which is as deep as the
104
- # generated document nests them.
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.
105
107
  #
106
108
  # @param schema [Hash, nil] a schema that may be a reference
107
109
  # @return [Hash, nil] the schema it names
108
110
  def resolve_schema(schema, depth = 0)
109
111
  return unless schema.is_a?(Hash)
110
- return schema unless schema.key?('$ref')
111
112
  return if depth > 3
113
+ return merge_all_of(schema['allOf'], depth) if schema.key?('allOf')
114
+ return schema unless schema.key?('$ref')
112
115
 
113
116
  name = schema['$ref'].to_s.split('/').last
114
117
  resolve_schema(document.dig('components', 'schemas', name), depth + 1)
115
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
116
128
  end
117
129
  end
118
130
  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
@@ -34,7 +34,7 @@ module Spree
34
34
  catalog_product price_list_product catalog_assignment catalog_quantity_rule catalog_product_term
35
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 payment_capture_event 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
@@ -0,0 +1,118 @@
1
+ module Spree
2
+ module Api
3
+ # Renders every value list the serializers name (`enum_type_name:`, see
4
+ # {Typelizer::OpenEnums}) into `@spree/core`, once, as both the values and
5
+ # the type — open unless the name is one of `CLOSED_LIST_NAMES`. Each SDK's `Enums.ts` re-exports the type
6
+ # from there instead of carrying its own copy, and the dashboards read the
7
+ # values instead of repeating them by hand.
8
+ #
9
+ # Run by `rake typelizer:generate`; `enum_types_spec.rb` fails when the
10
+ # committed file no longer matches the serializers.
11
+ class EnumTypes
12
+ class ConflictError < StandardError; end
13
+
14
+ HEADER = "// This file is auto-generated from the API serializers by `rake typelizer:generate`. Do not edit directly.\n".freeze
15
+ PATH = 'packages/core/src/enums.generated.ts'.freeze
16
+ LINE_WIDTH = 100
17
+
18
+ # @param properties [Array<Typelizer::Property>] every named enum property, across all serializers
19
+ def initialize(properties = self.class.serializer_properties)
20
+ @properties = properties
21
+ end
22
+
23
+ # Walks every property rather than Typelizer's own `enum_types`, which
24
+ # keeps one property per name within a serializer and so would hide two
25
+ # attributes of one serializer that disagree.
26
+ #
27
+ # @return [Array<Typelizer::Property>] the named enum properties of every serializer each Typelizer writer emits
28
+ def self.serializer_properties
29
+ Typelizer.configuration.writers.keys
30
+ .flat_map { |writer_name| Typelizer.interfaces(writer_name: writer_name) }
31
+ .flat_map { |interface| named_properties(interface.properties + interface.trait_interfaces.flat_map(&:properties)) }
32
+ end
33
+
34
+ # @param properties [Array<Typelizer::Property>]
35
+ # @return [Array<Typelizer::Property>] those carrying a named list, nested and inline ones included
36
+ def self.named_properties(properties)
37
+ properties.flat_map do |property|
38
+ nested = property.nested_properties.to_a
39
+ nested += property.type.properties if property.type.is_a?(Typelizer::Interface) && property.type.inline?
40
+
41
+ [property, *named_properties(nested)]
42
+ end.select { |property| property.enum && property.enum_type_name }
43
+ end
44
+
45
+ # @return [Hash{String => Array<String>}] built-in values keyed by type name
46
+ # @raise [ConflictError] when two serializers give one type name different values
47
+ def lists
48
+ @lists ||= @properties
49
+ .select(&:enum_type_name)
50
+ .reject { |property| Typelizer::OpenEnums::CLOSED_TYPE_NAMES.include?(property.enum_type_name) }
51
+ .group_by(&:enum_type_name)
52
+ .sort
53
+ .to_h { |name, properties| [name, values_of(name, properties)] }
54
+ end
55
+
56
+ # @return [Array<String>] closed list names no serializer declares any more
57
+ def stale_closed_names
58
+ Typelizer::OpenEnums::CLOSED_LIST_NAMES - lists.keys
59
+ end
60
+
61
+ # @return [String] the file's contents
62
+ def render
63
+ HEADER + lists.map { |name, values| definition(name, values) }.join + built_in_status
64
+ end
65
+
66
+ # @param root [Pathname] the monorepo root
67
+ # @return [void]
68
+ # @raise [ConflictError] when `CLOSED_LIST_NAMES` names a list no serializer declares
69
+ def write!(root)
70
+ raise ConflictError, "No serializer names #{stale_closed_names.join(', ')}; remove it from CLOSED_LIST_NAMES." if stale_closed_names.any?
71
+
72
+ File.write(root.join(PATH), render)
73
+ end
74
+
75
+ # @param name [String] a type name, e.g. `ProductStatus`
76
+ # @return [String] the constant holding its values, e.g. `PRODUCT_STATUSES`
77
+ def self.constant_name(name)
78
+ name.underscore.pluralize.upcase
79
+ end
80
+
81
+ private
82
+
83
+ def values_of(name, properties)
84
+ variants = properties.map { |property| property.enum.map(&:to_s) }.uniq
85
+ return variants.first if variants.one?
86
+
87
+ raise ConflictError, "#{name} is declared with different values: " \
88
+ "#{variants.map { |values| values.join(', ') }.join(' / ')}. " \
89
+ 'Give every serializer the same list, or give the lists different names.'
90
+ end
91
+
92
+ # Every built-in value of every status list, so a surface that styles
93
+ # statuses can be checked for one the server added.
94
+ def built_in_status
95
+ members = lists.keys.grep(/Status\z/).map { |name| " | (typeof #{self.class.constant_name(name)})[number]\n" }
96
+ return '' if members.empty?
97
+
98
+ "\n/** Every built-in value of every status list above. */\nexport type BuiltInStatus =\n#{members.join}"
99
+ end
100
+
101
+ def definition(name, values)
102
+ constant = self.class.constant_name(name)
103
+ quoted = values.map { |value| "'#{value}'" }
104
+ line = "export const #{constant} = [#{quoted.join(', ')}] as const"
105
+ line = "export const #{constant} = [\n#{quoted.map { |value| " #{value},\n" }.join}] as const" if line.length > LINE_WIDTH
106
+
107
+ if Typelizer::OpenEnums.closed?(name)
108
+ "\n/** Every value of `#{name}`. */\n#{line}\nexport type #{name} = (typeof #{constant})[number]\n"
109
+ else
110
+ type = "export type #{name} = (typeof #{constant})[number] | (string & {})"
111
+ type = "export type #{name} =\n | (typeof #{constant})[number]\n | (string & {})" if type.length > LINE_WIDTH
112
+
113
+ "\n/** The built-in values of `#{name}`. Extensions may add more. */\n#{line}\n#{type}\n"
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -106,13 +106,20 @@ module Spree
106
106
  def attribute_rows(table)
107
107
  rows = ['| Attribute | Kind | Predicates |', '|---|---|---|']
108
108
  table.attributes.except(*table.aliased_attributes).each do |attribute, kind|
109
- label = kind.is_a?(Hash) ? "One of #{kind['enum'].map { |value| "`#{value}`" }.join(', ')}" : KIND_LABELS.fetch(kind)
109
+ label = kind_label(kind)
110
110
  predicates = Spree::Api::V3::FilterPredicates::BY_KIND.fetch(kind.is_a?(Hash) ? 'enum' : kind)
111
111
  rows << "| `#{attribute}` | #{label} | #{predicates.map { |predicate| "`#{predicate}`" }.join(' ')} |"
112
112
  end
113
113
  rows
114
114
  end
115
115
 
116
+ def kind_label(kind)
117
+ return KIND_LABELS.fetch(kind) unless kind.is_a?(Hash)
118
+ return "One of the `#{kind['code']}` codes" if kind['code']
119
+
120
+ "One of #{kind['enum'].map { |value| "`#{value}`" }.join(', ')}"
121
+ end
122
+
116
123
  def association_rows
117
124
  visible = ->(table) { table.attributes.keys - table.aliased_attributes }
118
125
  @table.associations.flat_map do |name, associated|