e_plat 1.3.0 → 1.5.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b76ee9e7103fc4e5cb491cb07cd8fd8cf24cc56d28e16cc99d222da3b65db77
4
- data.tar.gz: 623f8ef5b141c3b5b24138c7abca5610aec3d7acb840a749e6481320eeff890d
3
+ metadata.gz: 1f97875697b04e12d0fd6514ae9198d03022199c064d0c752ffa60a7f024c13e
4
+ data.tar.gz: e8ebc93252048ff24b128d3f46125d6709fccf66dc95578ab5267bb842d0011c
5
5
  SHA512:
6
- metadata.gz: 878c953fc1ac93d857cbae808664affb031f68d1a9c5198059773373c5c74f9ab7139937ccab4199ae362af3b4bf80e0969a34de972eb3f80e25d7a95aeef92d
7
- data.tar.gz: 59679a5b7d68e46a15811e9591e052697c14589b0dbee5597bca550e595fd48878a0e9415f7a55a876a56f676a26a1a93d39699e8cee1126efa0d5f5c5b342b8
6
+ metadata.gz: 0405d140e821f967cd7266006fd79881b8da73c6fe9bba9e82f337326d5f604392627e7c77e9e399a3a4b278aa8daee2590ca667b2834344e740799acce462a0
7
+ data.tar.gz: 4305d78ed5fb650cd21034bd450c7fca9ed3cdf4af8432202b2dfc888fc1ee2f9ef6c2d97e3a74f7044d110fffc03cd7255e2ac9abdad09b8fbdf7e65e94c2a6
@@ -8,6 +8,7 @@ module EPlat
8
8
  EPlat::Shopify::Product
9
9
  EPlat::Shopify::Product::Variant
10
10
  )
11
+ GRAPHQL_PAGING_PARAMS = %w(after before first last limit).freeze
11
12
 
12
13
  def initialize(parsed = [])
13
14
  super parsed
@@ -56,8 +57,8 @@ module EPlat
56
57
  url = "#{ resource_class.collection_path }#{ url }" if client.bigcommerce?
57
58
 
58
59
  if paginates_via_graphql?
59
- arg_name = (url == @next) ? "after" : "before"
60
- resource_class.find(:all, params: {arg_name => url, first: 100})
60
+ cursor_param = (url == @next) ? "after" : "before"
61
+ resource_class.find(:all, params: params_for_another_graphql_page(cursor_param, url))
61
62
  elsif client.woocommerce?
62
63
  page_number = extract_page_from_url(url)
63
64
  resource_class.find(:all, params: {page: page_number})
@@ -66,6 +67,12 @@ module EPlat
66
67
  end
67
68
  end
68
69
 
70
+ def params_for_another_graphql_page(cursor_param, cursor)
71
+ filter_params = (original_params || {}).to_h.reject { |key, _value| GRAPHQL_PAGING_PARAMS.include?(key.to_s) }
72
+
73
+ filter_params.merge(cursor_param => cursor, first: 100)
74
+ end
75
+
69
76
  def pagination_links
70
77
  return pagination_cursors if paginates_via_graphql?
71
78
 
@@ -10,6 +10,7 @@ module EPlat
10
10
 
11
11
  FILTER_ARGS = [:id, :after, :before, :last, :first, :sort_key, :reverse].freeze
12
12
  QUERY_ARG_ARGS = [:available_for_sale, :created_at, :product_type, :product_id, :tag, :tag_not, :title, :updated_at, :vendor].freeze
13
+ UserErrorsResponse = Struct.new(:body)
13
14
 
14
15
  class_methods do
15
16
 
@@ -60,11 +61,15 @@ module EPlat
60
61
  else
61
62
  case action
62
63
  when :find_one, :find_single
64
+ raise_resource_not_found! if resource_missing_from?(response)
65
+
63
66
  instantiate_record resources_parsed_from(response)
64
67
  else
65
- instantiate_collection resources_parsed_from(response)
68
+ instantiate_collection resources_parsed_from(response), args
66
69
  end
67
70
  end
71
+ rescue ActiveResource::ResourceNotFound
72
+ raise
68
73
  rescue StandardError => e
69
74
  raise EPlat::GraphqlError.new(e.message)
70
75
  end
@@ -79,6 +84,17 @@ module EPlat
79
84
  ).body
80
85
  end
81
86
 
87
+ # a 200 with a null resource is Shopify graphql's version of a 404
88
+ def resource_missing_from?(response)
89
+ data = response["data"].is_a?(Hash) ? response["data"] : response
90
+
91
+ data.key?(graphql_element_name) && data[graphql_element_name].nil?
92
+ end
93
+
94
+ def raise_resource_not_found!
95
+ raise ActiveResource::ResourceNotFound.new(nil, "#{name} not found")
96
+ end
97
+
82
98
  #utils
83
99
 
84
100
  def scope_with_options_hash(args, action)
@@ -214,6 +230,13 @@ module EPlat
214
230
 
215
231
  end
216
232
 
233
+ def save!
234
+ return true if save
235
+
236
+ invalid_response = UserErrorsResponse.new({message: errors.full_messages.join(", ")}.to_json)
237
+ raise ActiveResource::ResourceInvalid.new(invalid_response)
238
+ end
239
+
217
240
  def mutate_graphql(action, graphql_proc_constant, *args)
218
241
  callback_name = (action == :delete) ? :destroy : action
219
242
 
@@ -224,6 +247,9 @@ module EPlat
224
247
  raise EPlat::GraphqlError.new(response.first["message"])
225
248
  elsif response["errors"]
226
249
  raise EPlat::GraphqlError.new(response["errors"].first["message"])
250
+ elsif (user_errors_by_mutation = user_errors_by_mutation_from(response)).any?
251
+ log_user_errors(user_errors_by_mutation)
252
+ raise_resource_invalid_with(user_errors_by_mutation.values.flatten)
227
253
  else
228
254
  response = remove_mutation_root_from(response, graphql_proc_constant)
229
255
  parsed_response = self.class.resources_parsed_from(response)
@@ -238,6 +264,8 @@ module EPlat
238
264
  changed_attributes.clear
239
265
  end
240
266
  end
267
+ rescue ActiveResource::ResourceInvalid
268
+ raise
241
269
  rescue StandardError => e
242
270
  raise EPlat::GraphqlError.new(e.message)
243
271
  end
@@ -251,6 +279,46 @@ module EPlat
251
279
  mutation_proc.call(input, additional_graphql:)
252
280
  end
253
281
 
282
+ # one request can carry several mutation roots, e.g. productUpdate plus productVariantsBulkUpdate
283
+ def user_errors_by_mutation_from(response)
284
+ data = response["data"].is_a?(Hash) ? response["data"] : response
285
+ mutation_payloads = data.select { |_mutation_name, payload| payload.is_a?(Hash) }
286
+
287
+ mutation_payloads
288
+ .transform_values { |payload| payload["userErrors"] || [] }
289
+ .reject { |_mutation_name, user_errors| user_errors.empty? }
290
+ end
291
+
292
+ def log_user_errors(user_errors_by_mutation)
293
+ user_errors_by_mutation.each do |mutation_name, user_errors|
294
+ user_errors_logger&.warn(
295
+ "[EPlat] #{mutation_name} userErrors on #{self.class.name} #{id.inspect}: #{user_errors_message(user_errors)}"
296
+ )
297
+ end
298
+ end
299
+
300
+ def user_errors_logger
301
+ defined?(Rails) && Rails.logger ? Rails.logger : self.class.logger
302
+ end
303
+
304
+ def user_errors_message(user_errors)
305
+ user_errors.map do |user_error|
306
+ field = Array(user_error["field"]).join(".")
307
+
308
+ [field.presence, user_error["message"]].compact.join(": ")
309
+ end.join(", ")
310
+ end
311
+
312
+ # ActiveResource's save rescues ResourceInvalid into errors and returns false; save! re-raises it
313
+ def raise_resource_invalid_with(user_errors)
314
+ error_messages = user_errors.map { |user_error| user_error["message"] }
315
+ invalid_response = UserErrorsResponse.new(
316
+ {errors: {base: error_messages}, message: error_messages.join(", ")}.to_json
317
+ )
318
+
319
+ raise ActiveResource::ResourceInvalid.new(invalid_response)
320
+ end
321
+
254
322
  def remove_mutation_root_from(data, graphql_proc_constant)
255
323
  mutation_root = graphql_proc_constant.split(".").last.camelcase(:lower)
256
324
  (mutation_root && data[mutation_root]) ? data[mutation_root] : data
@@ -47,6 +47,10 @@ module EPlat::Shopify::GraphQL::V202604::Mutation::Product::Variant
47
47
  mutation {
48
48
  #{additional_graphql}
49
49
  productVariantsBulkDelete(#{args}) {
50
+ userErrors {
51
+ field
52
+ message
53
+ }
50
54
  product {
51
55
  id
52
56
  }
@@ -61,6 +65,10 @@ module EPlat::Shopify::GraphQL::V202604::Mutation::Product::Variant
61
65
  <<~GRAPHQL
62
66
  #{additional_graphql}
63
67
  productVariantsBulkCreate(#{args}) {
68
+ userErrors {
69
+ field
70
+ message
71
+ }
64
72
  productVariants {
65
73
  #{EPlat::Shopify::Product::Variant.graphql_fields}
66
74
  }
@@ -74,6 +82,10 @@ module EPlat::Shopify::GraphQL::V202604::Mutation::Product::Variant
74
82
  <<~GRAPHQL
75
83
  #{additional_graphql}
76
84
  productVariantsBulkUpdate(#{args}) {
85
+ userErrors {
86
+ field
87
+ message
88
+ }
77
89
  productVariants {
78
90
  #{EPlat::Shopify::Product::Variant.graphql_fields}
79
91
  }
@@ -10,6 +10,10 @@ module EPlat::Shopify::GraphQL::V202604::Mutation::Product
10
10
  mutation {
11
11
  #{additional_graphql}
12
12
  productCreate(#{args}) {
13
+ userErrors {
14
+ field
15
+ message
16
+ }
13
17
  product {
14
18
  #{EPlat::Shopify::Product.graphql_fields}
15
19
  }
@@ -25,6 +29,10 @@ module EPlat::Shopify::GraphQL::V202604::Mutation::Product
25
29
  mutation {
26
30
  #{additional_graphql}
27
31
  productUpdate(#{args}) {
32
+ userErrors {
33
+ field
34
+ message
35
+ }
28
36
  product {
29
37
  #{EPlat::Shopify::Product.graphql_fields}
30
38
  }
@@ -1,3 +1,3 @@
1
1
  module EPlat
2
- VERSION = "1.3.0"
2
+ VERSION = "1.5.0"
3
3
  end
data/lib/e_plat.rb CHANGED
@@ -1,13 +1,15 @@
1
- require "e_plat/version"
2
- require "e_plat/engine"
3
- require "zeitwerk"
1
+ # frozen_string_literal: true
4
2
 
5
- require "active_support"
6
- require "activeresource"
7
- require "activeresource-response"
3
+ require 'e_plat/version'
4
+ require 'e_plat/engine'
5
+ require 'zeitwerk'
6
+
7
+ require 'active_support'
8
+ require 'activeresource'
9
+ require 'activeresource-response'
8
10
  require 'dry-types'
9
11
  require 'active_resource/connection_error'
10
- require_relative "hash"
12
+ require_relative 'hash'
11
13
  # require "e_plat/railtie" if defined?(Rails::Railtie)
12
14
 
13
15
  Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
@@ -16,66 +18,64 @@ loader.collapse("#{__dir__}/e_plat/resource") # So can call EPlat::Product
16
18
  loader.collapse("#{__dir__}/e_plat/resource/shopify_only")
17
19
  loader.collapse("#{__dir__}/e_plat/resource/platform_specific")
18
20
  loader.collapse("#{__dir__}/e_plat/errors")
19
- loader.setup
21
+ loader.setup
20
22
 
23
+ module EPlat
24
+ extend Dry::Configurable
25
+ SUPPORTED_SHOPIFY_API_VERSIONS = %w[2024_01 2024_07 2025_01 2025_10 2026_04].freeze
26
+ SUPPORTED_BIGCOMMERCE_API_VERSIONS = ['3'].freeze
27
+ SUPPORTED_WOOCOMMERCE_API_VERSIONS = ['3'].freeze
28
+ SUPPORTED_PLATFORMS = %w[shopify bigcommerce woocommerce].freeze
29
+ SUPPORTED_RESOURCES = [
30
+ 'shop',
31
+ 'product',
32
+ 'product/variant',
33
+ 'product/image',
34
+ 'product/option',
35
+ 'product/image',
36
+ 'order',
37
+ 'order/line_item',
38
+ 'order/shipping_address',
39
+ 'order/billing_address',
40
+ 'script_tag',
41
+ 'webhook',
42
+ 'metafield'
43
+ ].freeze
44
+ PROTECTED_PREFIX = '_protected_'
21
45
 
46
+ setting(:shopify_api_version, default: ENV['EPLAT_SHOPIFY_API_VERSION'] || '2026_04', constructor: lambda do |value|
47
+ SUPPORTED_SHOPIFY_API_VERSIONS.include?(value.underscore) ? value.underscore : raise(ArgumentError, "Shopify API version #{value} is not supported")
48
+ end)
22
49
 
23
- module EPlat
24
- extend Dry::Configurable
25
- SUPPORTED_SHOPIFY_API_VERSIONS = ['2024_01', '2024_07', '2025_01', '2025_10', '2026_04']
26
- SUPPORTED_BIGCOMMERCE_API_VERSIONS = ['3']
27
- SUPPORTED_WOOCOMMERCE_API_VERSIONS = ['3']
28
- SUPPORTED_PLATFORMS = ["shopify", "bigcommerce", "woocommerce"]
29
- SUPPORTED_RESOURCES = [
30
- "shop",
31
- "product",
32
- "product/variant",
33
- "product/image",
34
- "product/option",
35
- "product/image",
36
- "order",
37
- "order/line_item",
38
- "order/shipping_address",
39
- "order/billing_address",
40
- "script_tag",
41
- "webhook",
42
- "metafield"
43
- ]
44
- PROTECTED_PREFIX = "_protected_"
45
-
46
- setting(:shopify_api_version, default: ENV['EPLAT_SHOPIFY_API_VERSION'] || "2026_04", constructor: ->(value) do
47
- SUPPORTED_SHOPIFY_API_VERSIONS.include?(value.underscore) ? value.underscore : raise(ArgumentError, "Shopify API version #{value} is not supported")
48
- end)
50
+ setting(:bigcommerce_api_version, default: ENV['EPLAT_BIGCOMMERCE_API_VERSION'] || 'v3', constructor: lambda do |value|
51
+ v = value.downcase.gsub('v', '')
52
+ SUPPORTED_BIGCOMMERCE_API_VERSIONS.include?(v) ? v : raise(ArgumentError, "Bigcommerce API version #{value} is not supported")
53
+ end)
49
54
 
50
- setting(:bigcommerce_api_version, default: ENV['EPLAT_BIGCOMMERCE_API_VERSION'] || "v3", constructor: ->(value) do
51
- v = value.downcase.gsub("v", "")
52
- SUPPORTED_BIGCOMMERCE_API_VERSIONS.include?(v) ? v : raise(ArgumentError, "Bigcommerce API version #{value} is not supported")
53
- end)
54
-
55
- setting(:woocommerce_api_version, default: ENV['EPLAT_WOOCOMMERCE_API_VERSION'] || "v3", constructor: ->(value) do
56
- v = value.downcase.gsub("v", "")
57
- SUPPORTED_WOOCOMMERCE_API_VERSIONS.include?(v) ? v : raise(ArgumentError, "WooCommerce API version #{value} is not supported")
58
- end)
55
+ setting(:woocommerce_api_version, default: ENV['EPLAT_WOOCOMMERCE_API_VERSION'] || 'v3', constructor: lambda do |value|
56
+ v = value.downcase.gsub('v', '')
57
+ SUPPORTED_WOOCOMMERCE_API_VERSIONS.include?(v) ? v : raise(ArgumentError, "WooCommerce API version #{value} is not supported")
58
+ end)
59
59
 
60
- setting(:print_graphql_requests, default:ENV['EPLAT_PRINT_GRAPHQL_REQUESTS'] || false)
60
+ setting(:print_graphql_requests, default: ENV['EPLAT_PRINT_GRAPHQL_REQUESTS'] || false)
61
61
 
62
- setting(:mute_woocommerce_variant_warnings, default: ENV['EPLAT_MUTE_WOOCOMMERCE_VARIANT_WARNINGS'] || false)
62
+ setting(:mute_woocommerce_variant_warnings, default: ENV['EPLAT_MUTE_WOOCOMMERCE_VARIANT_WARNINGS'] || false)
63
63
 
64
- def self.api_display_name
65
- apis = Struct.new(:shopify, :bigcommerce, :woocommerce)
66
- apis.new(
67
- shopify: config.shopify_api_version&.dasherize, # "2024-01"
68
- bigcommerce: "v#{config.bigcommerce_api_version}", # "v3"
69
- woocommerce: "v#{config.woocommerce_api_version}" # "v3"
70
- )
71
- end
64
+ def self.api_display_name
65
+ apis = Struct.new(:shopify, :bigcommerce, :woocommerce)
66
+ apis.new(
67
+ shopify: config.shopify_api_version&.dasherize, # "2024-01"
68
+ bigcommerce: "v#{config.bigcommerce_api_version}", # "v3"
69
+ woocommerce: "v#{config.woocommerce_api_version}" # "v3"
70
+ )
71
+ end
72
72
 
73
- def self.shopify_graphql_version
74
- "V#{ EPlat.config.shopify_api_version.gsub("_", "") }"
75
- end
73
+ def self.shopify_graphql_version
74
+ "V#{EPlat.config.shopify_api_version.gsub('_', '')}"
75
+ end
76
76
 
77
- class Error < StandardError; end
78
- class GraphqlError < Error; end
77
+ class Error < StandardError; end
78
+ class GraphqlError < Error; end
79
79
  end
80
80
 
81
81
  EPlat::Initializer.check_required_classes!
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: e_plat
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - oliwoodsuk
@@ -503,7 +503,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
503
503
  - !ruby/object:Gem::Version
504
504
  version: '0'
505
505
  requirements: []
506
- rubygems_version: 4.0.5
506
+ rubygems_version: 4.0.10
507
507
  specification_version: 4
508
508
  summary: Creates a single interface for integrating with ecommerce platform APIs.
509
509
  test_files: []