active_shopify_graphql 0.4.0 → 0.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/README.md +158 -56
  4. data/lib/active_shopify_graphql/configuration.rb +2 -15
  5. data/lib/active_shopify_graphql/connections/connection_loader.rb +141 -0
  6. data/lib/active_shopify_graphql/gid_helper.rb +2 -0
  7. data/lib/active_shopify_graphql/loader.rb +147 -126
  8. data/lib/active_shopify_graphql/loader_context.rb +2 -47
  9. data/lib/active_shopify_graphql/loader_proxy.rb +67 -0
  10. data/lib/active_shopify_graphql/loaders/admin_api_loader.rb +1 -17
  11. data/lib/active_shopify_graphql/loaders/customer_account_api_loader.rb +10 -26
  12. data/lib/active_shopify_graphql/model/associations.rb +94 -0
  13. data/lib/active_shopify_graphql/model/attributes.rb +48 -0
  14. data/lib/active_shopify_graphql/model/connections.rb +174 -0
  15. data/lib/active_shopify_graphql/model/finder_methods.rb +155 -0
  16. data/lib/active_shopify_graphql/model/graphql_type_resolver.rb +89 -0
  17. data/lib/active_shopify_graphql/model/loader_switchable.rb +79 -0
  18. data/lib/active_shopify_graphql/model/metafield_attributes.rb +59 -0
  19. data/lib/active_shopify_graphql/{base.rb → model.rb} +30 -16
  20. data/lib/active_shopify_graphql/model_builder.rb +53 -0
  21. data/lib/active_shopify_graphql/query/node/collection.rb +78 -0
  22. data/lib/active_shopify_graphql/query/node/connection.rb +16 -0
  23. data/lib/active_shopify_graphql/query/node/current_customer.rb +37 -0
  24. data/lib/active_shopify_graphql/query/node/field.rb +23 -0
  25. data/lib/active_shopify_graphql/query/node/fragment.rb +17 -0
  26. data/lib/active_shopify_graphql/query/node/nested_connection.rb +79 -0
  27. data/lib/active_shopify_graphql/query/node/raw.rb +15 -0
  28. data/lib/active_shopify_graphql/query/node/root_connection.rb +76 -0
  29. data/lib/active_shopify_graphql/query/node/single_record.rb +36 -0
  30. data/lib/active_shopify_graphql/query/node/singular.rb +16 -0
  31. data/lib/active_shopify_graphql/query/node.rb +95 -0
  32. data/lib/active_shopify_graphql/query/query_builder.rb +290 -0
  33. data/lib/active_shopify_graphql/query/relation.rb +424 -0
  34. data/lib/active_shopify_graphql/query/scope.rb +219 -0
  35. data/lib/active_shopify_graphql/response/page_info.rb +40 -0
  36. data/lib/active_shopify_graphql/response/paginated_result.rb +114 -0
  37. data/lib/active_shopify_graphql/response/response_mapper.rb +242 -0
  38. data/lib/active_shopify_graphql/search_query/hash_condition_formatter.rb +107 -0
  39. data/lib/active_shopify_graphql/search_query/parameter_binder.rb +68 -0
  40. data/lib/active_shopify_graphql/search_query/value_sanitizer.rb +24 -0
  41. data/lib/active_shopify_graphql/search_query.rb +34 -84
  42. data/lib/active_shopify_graphql/version.rb +1 -1
  43. data/lib/active_shopify_graphql.rb +29 -29
  44. metadata +46 -15
  45. data/lib/active_shopify_graphql/associations.rb +0 -94
  46. data/lib/active_shopify_graphql/attributes.rb +0 -50
  47. data/lib/active_shopify_graphql/connection_loader.rb +0 -96
  48. data/lib/active_shopify_graphql/connections.rb +0 -198
  49. data/lib/active_shopify_graphql/finder_methods.rb +0 -182
  50. data/lib/active_shopify_graphql/fragment_builder.rb +0 -228
  51. data/lib/active_shopify_graphql/graphql_type_resolver.rb +0 -91
  52. data/lib/active_shopify_graphql/includes_scope.rb +0 -48
  53. data/lib/active_shopify_graphql/loader_switchable.rb +0 -180
  54. data/lib/active_shopify_graphql/metafield_attributes.rb +0 -61
  55. data/lib/active_shopify_graphql/query_node.rb +0 -173
  56. data/lib/active_shopify_graphql/query_tree.rb +0 -225
  57. data/lib/active_shopify_graphql/response_mapper.rb +0 -249
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "value_sanitizer"
4
+
5
+ module ActiveShopifyGraphQL
6
+ class SearchQuery
7
+ # Formats hash-based query conditions with proper sanitization
8
+ class HashConditionFormatter
9
+ # Formats hash conditions into a Shopify search query string
10
+ # @param conditions [Hash] The conditions to format
11
+ # @return [String] The formatted query string
12
+ def self.format(conditions)
13
+ return "" if conditions.empty?
14
+
15
+ query_parts = conditions.map do |key, value|
16
+ format_condition(key.to_s, value)
17
+ end
18
+
19
+ query_parts.join(" AND ")
20
+ end
21
+
22
+ # Formats a single query condition
23
+ # @param key [String] The attribute name
24
+ # @param value [Object] The attribute value
25
+ # @return [String] The formatted query condition
26
+ def self.format_condition(key, value)
27
+ case value
28
+ when Array
29
+ format_array_condition(key, value)
30
+ when String
31
+ format_string_condition(key, value)
32
+ when Numeric, true, false
33
+ "#{key}:#{value}"
34
+ when Hash
35
+ format_range_condition(key, value)
36
+ else
37
+ "#{key}:#{value}"
38
+ end
39
+ end
40
+
41
+ # Formats an array condition with OR clauses
42
+ # @param key [String] The attribute name
43
+ # @param values [Array] The array of values
44
+ # @return [String] The formatted query with OR clauses wrapped in parentheses
45
+ def self.format_array_condition(key, values)
46
+ return "" if values.empty?
47
+ return format_condition(key, values.first) if values.size == 1
48
+
49
+ or_parts = values.map do |value|
50
+ format_single_value(key, value)
51
+ end
52
+
53
+ "(#{or_parts.join(' OR ')})"
54
+ end
55
+
56
+ # Formats a single value for use in array OR clauses
57
+ # @param key [String] The attribute name
58
+ # @param value [Object] The attribute value
59
+ # @return [String] The formatted key:value pair
60
+ def self.format_single_value(key, value)
61
+ case value
62
+ when String
63
+ format_string_condition(key, value)
64
+ when Numeric, true, false
65
+ "#{key}:#{value}"
66
+ else
67
+ "#{key}:#{value}"
68
+ end
69
+ end
70
+
71
+ # Formats a string condition with proper quoting and sanitization
72
+ # @param key [String] The attribute name
73
+ # @param value [String] The string value
74
+ # @return [String] The formatted condition
75
+ def self.format_string_condition(key, value)
76
+ escaped_value = ValueSanitizer.sanitize(value)
77
+ "#{key}:'#{escaped_value}'"
78
+ end
79
+
80
+ # Formats a range condition (e.g., { created_at: { gte: '2024-01-01' } })
81
+ # @param key [String] The attribute name
82
+ # @param value [Hash] The range conditions
83
+ # @return [String] The formatted range condition
84
+ def self.format_range_condition(key, value)
85
+ range_parts = value.map do |operator, range_value|
86
+ case operator.to_sym
87
+ when :gt, :>
88
+ "#{key}:>#{range_value}"
89
+ when :gte, :>=
90
+ "#{key}:>=#{range_value}"
91
+ when :lt, :<
92
+ "#{key}:<#{range_value}"
93
+ when :lte, :<=
94
+ "#{key}:<=#{range_value}"
95
+ else
96
+ raise ArgumentError, "Unsupported range operator: #{operator}"
97
+ end
98
+ end
99
+ range_parts.join(" ")
100
+ end
101
+
102
+ private_class_method :format_condition, :format_array_condition,
103
+ :format_single_value, :format_string_condition,
104
+ :format_range_condition
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "value_sanitizer"
4
+
5
+ module ActiveShopifyGraphQL
6
+ class SearchQuery
7
+ # Binds parameters to string queries with proper sanitization
8
+ # Supports both positional (?) and named (:param) placeholders
9
+ class ParameterBinder
10
+ # Binds parameters to a query string
11
+ # @param query_string [String] The query with placeholders
12
+ # @param args [Array] Positional arguments or a hash of named parameters
13
+ # @return [String] The query with bound and escaped parameters
14
+ def self.bind(query_string, *args)
15
+ return query_string if args.empty?
16
+
17
+ if args.first.is_a?(Hash)
18
+ bind_named_parameters(query_string, args.first)
19
+ else
20
+ bind_positional_parameters(query_string, args)
21
+ end
22
+ end
23
+
24
+ # Binds positional parameters (?)
25
+ # @param query_string [String] The query with ? placeholders
26
+ # @param values [Array] The values to bind
27
+ # @return [String] The query with bound parameters
28
+ def self.bind_positional_parameters(query_string, values)
29
+ result = query_string.dup
30
+ values.each do |value|
31
+ result = result.sub("?", format_value(value))
32
+ end
33
+ result
34
+ end
35
+
36
+ # Binds named parameters (:name)
37
+ # @param query_string [String] The query with :name placeholders
38
+ # @param params [Hash] The parameters hash
39
+ # @return [String] The query with bound parameters
40
+ def self.bind_named_parameters(query_string, params)
41
+ result = query_string.dup
42
+ params.each do |key, value|
43
+ placeholder = ":#{key}"
44
+ result = result.gsub(placeholder, format_value(value))
45
+ end
46
+ result
47
+ end
48
+
49
+ # Formats a value for safe insertion into query
50
+ # @param value [Object] The value to format
51
+ # @return [String] The formatted value
52
+ def self.format_value(value)
53
+ case value
54
+ when String
55
+ "'#{ValueSanitizer.sanitize(value)}'"
56
+ when Numeric, true, false
57
+ value.to_s
58
+ when nil
59
+ "null"
60
+ else
61
+ "'#{ValueSanitizer.sanitize(value.to_s)}'"
62
+ end
63
+ end
64
+
65
+ private_class_method :bind_positional_parameters, :bind_named_parameters, :format_value
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveShopifyGraphQL
4
+ class SearchQuery
5
+ # Sanitizes values by escaping special characters for Shopify search syntax
6
+ class ValueSanitizer
7
+ # Sanitizes a value by escaping special characters
8
+ # @param value [String] The value to sanitize
9
+ # @return [String] The sanitized value
10
+ def self.sanitize(value)
11
+ value
12
+ .gsub('\\', '\\\\\\\\') # Escape backslashes first: \ becomes \\
13
+ .gsub('"', '\\"') # Escape double quotes with a single backslash
14
+ # Escape single quotes: O'Reilly becomes O\\'Reilly
15
+ # Why 4 backslashes? The escaping happens in layers:
16
+ # 1. Ruby string literal: "\\\\\\\\''" = literal string "\\\\''"
17
+ # 2. String interpolation in "#{key}:'#{escaped_value}'": the \\\' becomes \\'
18
+ # 3. Final GraphQL query: customers(query: "title:'O\\'Reilly'")
19
+ # The double backslash is required by Shopify's search syntax to properly escape the single quote
20
+ .gsub("'", "\\\\\\\\'")
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,103 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "search_query/hash_condition_formatter"
4
+ require_relative "search_query/parameter_binder"
5
+
3
6
  module ActiveShopifyGraphQL
4
7
  # Represents a Shopify search query, converting Ruby conditions into Shopify's search syntax
8
+ # Supports hash-based conditions (with sanitization), string-based conditions (raw), and parameter binding
9
+ #
10
+ # @example Hash-based query (safe, with sanitization)
11
+ # SearchQuery.new(sku: "ABC-123").to_s
12
+ # # => "sku:'ABC-123'"
13
+ #
14
+ # @example String-based query (raw, user responsibility for safety)
15
+ # SearchQuery.new("sku:* AND product_id:123").to_s
16
+ # # => "sku:* AND product_id:123"
17
+ #
18
+ # @example String with positional parameter binding
19
+ # SearchQuery.new("sku:? product_id:?", "Good ol' value", 123).to_s
20
+ # # => "sku:'Good ol\\' value' product_id:123"
21
+ #
22
+ # @example String with named parameter binding
23
+ # SearchQuery.new("sku::sku product_id::product_id", { sku: "A-SKU", product_id: 123 }).to_s
24
+ # # => "sku:'A-SKU' product_id:123"
5
25
  class SearchQuery
6
- def initialize(conditions = {})
26
+ def initialize(conditions = {}, *args)
7
27
  @conditions = conditions
28
+ @args = args
8
29
  end
9
30
 
10
31
  # Converts conditions to Shopify search query string
11
32
  # @return [String] The Shopify query string
12
33
  def to_s
13
- return "" if @conditions.empty?
14
-
15
- query_parts = @conditions.map do |key, value|
16
- format_condition(key.to_s, value)
17
- end
18
-
19
- query_parts.join(" AND ")
20
- end
21
-
22
- private
23
-
24
- # Formats a single query condition into Shopify's query syntax
25
- # @param key [String] The attribute name
26
- # @param value [Object] The attribute value
27
- # @return [String] The formatted query condition
28
- def format_condition(key, value)
29
- case value
30
- when Array
31
- format_array_condition(key, value)
32
- when String
33
- format_string_condition(key, value)
34
- when Numeric, true, false
35
- "#{key}:#{value}"
34
+ case @conditions
36
35
  when Hash
37
- format_range_condition(key, value)
38
- else
39
- "#{key}:#{value}"
40
- end
41
- end
42
-
43
- # Formats an array condition with OR clauses
44
- # @param key [String] The attribute name
45
- # @param values [Array] The array of values
46
- # @return [String] The formatted query with OR clauses wrapped in parentheses
47
- def format_array_condition(key, values)
48
- return "" if values.empty?
49
- return format_condition(key, values.first) if values.size == 1
50
-
51
- or_parts = values.map do |value|
52
- format_single_value(key, value)
53
- end
54
-
55
- "(#{or_parts.join(' OR ')})"
56
- end
57
-
58
- # Formats a single value for use in array OR clauses
59
- # @param key [String] The attribute name
60
- # @param value [Object] The attribute value
61
- # @return [String] The formatted key:value pair
62
- def format_single_value(key, value)
63
- case value
36
+ HashConditionFormatter.format(@conditions)
64
37
  when String
65
- format_string_condition(key, value)
66
- when Numeric, true, false
67
- "#{key}:#{value}"
68
- else
69
- "#{key}:#{value}"
70
- end
71
- end
72
-
73
- # Formats a string condition with proper quoting
74
- def format_string_condition(key, value)
75
- # Handle special string values and escape quotes
76
- if value.include?(" ") && !value.start_with?('"')
77
- # Multi-word values should be quoted
78
- "#{key}:\"#{value.gsub('"', '\\"')}\""
79
- else
80
- "#{key}:#{value}"
81
- end
82
- end
83
-
84
- # Formats a range condition (e.g., { created_at: { gte: '2024-01-01' } })
85
- def format_range_condition(key, value)
86
- range_parts = value.map do |operator, range_value|
87
- case operator.to_sym
88
- when :gt, :>
89
- "#{key}:>#{range_value}"
90
- when :gte, :>=
91
- "#{key}:>=#{range_value}"
92
- when :lt, :<
93
- "#{key}:<#{range_value}"
94
- when :lte, :<=
95
- "#{key}:<=#{range_value}"
38
+ if @args.empty?
39
+ @conditions
96
40
  else
97
- raise ArgumentError, "Unsupported range operator: #{operator}"
41
+ ParameterBinder.bind(@conditions, *@args)
98
42
  end
43
+ when Array
44
+ # Handle [query_string, *binding_args] format from FinderMethods#where
45
+ query_string = @conditions.first
46
+ binding_args = @conditions[1..]
47
+ ParameterBinder.bind(query_string, *binding_args)
48
+ else
49
+ ""
99
50
  end
100
- range_parts.join(" ")
101
51
  end
102
52
  end
103
53
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveShopifyGraphQL
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
@@ -1,37 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support'
4
- require 'active_support/inflector'
5
- require 'active_support/concern'
6
- require 'active_support/core_ext/object/blank'
7
- require 'active_model'
8
- require 'globalid'
3
+ require "active_support"
4
+ require "active_support/concern"
5
+ require "active_model"
6
+ require "active_model/attribute_assignment"
7
+ require "active_model/validations"
8
+ require "active_model/naming"
9
+ require "globalid"
9
10
 
10
- require_relative "active_shopify_graphql/version"
11
- require_relative "active_shopify_graphql/configuration"
12
- require_relative "active_shopify_graphql/gid_helper"
13
- require_relative "active_shopify_graphql/graphql_type_resolver"
14
- require_relative "active_shopify_graphql/loader_context"
15
- require_relative "active_shopify_graphql/query_node"
16
- require_relative "active_shopify_graphql/fragment_builder"
17
- require_relative "active_shopify_graphql/query_tree"
18
- require_relative "active_shopify_graphql/response_mapper"
19
- require_relative "active_shopify_graphql/connection_loader"
20
- require_relative "active_shopify_graphql/loader"
21
- require_relative "active_shopify_graphql/loaders/admin_api_loader"
22
- require_relative "active_shopify_graphql/loaders/customer_account_api_loader"
23
- require_relative "active_shopify_graphql/loader_switchable"
24
- require_relative "active_shopify_graphql/finder_methods"
25
- require_relative "active_shopify_graphql/associations"
26
- require_relative "active_shopify_graphql/graphql_associations"
27
- require_relative "active_shopify_graphql/includes_scope"
28
- require_relative "active_shopify_graphql/connections"
29
- require_relative "active_shopify_graphql/attributes"
30
- require_relative "active_shopify_graphql/metafield_attributes"
31
- require_relative "active_shopify_graphql/search_query"
32
- require_relative "active_shopify_graphql/base"
11
+ require "zeitwerk"
12
+ loader = Zeitwerk::Loader.for_gem
13
+ loader.inflector.inflect(
14
+ "active_shopify_graphql" => "ActiveShopifyGraphQL",
15
+ "graphql_associations" => "GraphQLAssociations"
16
+ )
17
+ loader.setup
33
18
 
34
19
  module ActiveShopifyGraphQL
35
20
  class Error < StandardError; end
36
21
  class ObjectNotFoundError < Error; end
22
+
23
+ class << self
24
+ def configuration
25
+ @configuration ||= Configuration.new
26
+ end
27
+
28
+ def configure
29
+ yield(configuration)
30
+ end
31
+
32
+ # Reset configuration (useful for testing)
33
+ def reset_configuration!
34
+ @configuration = Configuration.new
35
+ end
36
+ end
37
37
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_shopify_graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicolò Rebughini
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '1.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: zeitwerk
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '2.6'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '2.6'
55
69
  description: ActiveShopifyGraphQL provides an ActiveRecord-like interface for interacting
56
70
  with Shopify's GraphQL APIs, supporting both Admin API and Customer Account API
57
71
  with automatic query building and response mapping.
@@ -69,29 +83,46 @@ files:
69
83
  - README.md
70
84
  - Rakefile
71
85
  - lib/active_shopify_graphql.rb
72
- - lib/active_shopify_graphql/associations.rb
73
- - lib/active_shopify_graphql/attributes.rb
74
- - lib/active_shopify_graphql/base.rb
75
86
  - lib/active_shopify_graphql/configuration.rb
76
- - lib/active_shopify_graphql/connection_loader.rb
77
- - lib/active_shopify_graphql/connections.rb
87
+ - lib/active_shopify_graphql/connections/connection_loader.rb
78
88
  - lib/active_shopify_graphql/connections/connection_proxy.rb
79
- - lib/active_shopify_graphql/finder_methods.rb
80
- - lib/active_shopify_graphql/fragment_builder.rb
81
89
  - lib/active_shopify_graphql/gid_helper.rb
82
90
  - lib/active_shopify_graphql/graphql_associations.rb
83
- - lib/active_shopify_graphql/graphql_type_resolver.rb
84
- - lib/active_shopify_graphql/includes_scope.rb
85
91
  - lib/active_shopify_graphql/loader.rb
86
92
  - lib/active_shopify_graphql/loader_context.rb
87
- - lib/active_shopify_graphql/loader_switchable.rb
93
+ - lib/active_shopify_graphql/loader_proxy.rb
88
94
  - lib/active_shopify_graphql/loaders/admin_api_loader.rb
89
95
  - lib/active_shopify_graphql/loaders/customer_account_api_loader.rb
90
- - lib/active_shopify_graphql/metafield_attributes.rb
91
- - lib/active_shopify_graphql/query_node.rb
92
- - lib/active_shopify_graphql/query_tree.rb
93
- - lib/active_shopify_graphql/response_mapper.rb
96
+ - lib/active_shopify_graphql/model.rb
97
+ - lib/active_shopify_graphql/model/associations.rb
98
+ - lib/active_shopify_graphql/model/attributes.rb
99
+ - lib/active_shopify_graphql/model/connections.rb
100
+ - lib/active_shopify_graphql/model/finder_methods.rb
101
+ - lib/active_shopify_graphql/model/graphql_type_resolver.rb
102
+ - lib/active_shopify_graphql/model/loader_switchable.rb
103
+ - lib/active_shopify_graphql/model/metafield_attributes.rb
104
+ - lib/active_shopify_graphql/model_builder.rb
105
+ - lib/active_shopify_graphql/query/node.rb
106
+ - lib/active_shopify_graphql/query/node/collection.rb
107
+ - lib/active_shopify_graphql/query/node/connection.rb
108
+ - lib/active_shopify_graphql/query/node/current_customer.rb
109
+ - lib/active_shopify_graphql/query/node/field.rb
110
+ - lib/active_shopify_graphql/query/node/fragment.rb
111
+ - lib/active_shopify_graphql/query/node/nested_connection.rb
112
+ - lib/active_shopify_graphql/query/node/raw.rb
113
+ - lib/active_shopify_graphql/query/node/root_connection.rb
114
+ - lib/active_shopify_graphql/query/node/single_record.rb
115
+ - lib/active_shopify_graphql/query/node/singular.rb
116
+ - lib/active_shopify_graphql/query/query_builder.rb
117
+ - lib/active_shopify_graphql/query/relation.rb
118
+ - lib/active_shopify_graphql/query/scope.rb
119
+ - lib/active_shopify_graphql/response/page_info.rb
120
+ - lib/active_shopify_graphql/response/paginated_result.rb
121
+ - lib/active_shopify_graphql/response/response_mapper.rb
94
122
  - lib/active_shopify_graphql/search_query.rb
123
+ - lib/active_shopify_graphql/search_query/hash_condition_formatter.rb
124
+ - lib/active_shopify_graphql/search_query/parameter_binder.rb
125
+ - lib/active_shopify_graphql/search_query/value_sanitizer.rb
95
126
  - lib/active_shopify_graphql/version.rb
96
127
  homepage: https://github.com/nebulab/active_shopify_graphql
97
128
  licenses:
@@ -1,94 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveShopifyGraphQL
4
- # Handles associations between ActiveShopifyGraphQL objects and ActiveRecord objects
5
- module Associations
6
- extend ActiveSupport::Concern
7
-
8
- included do
9
- class << self
10
- attr_accessor :associations
11
- end
12
-
13
- self.associations = {}
14
- end
15
-
16
- class_methods do
17
- def has_many(name, class_name: nil, foreign_key: nil, primary_key: nil)
18
- association_class_name = class_name || name.to_s.classify
19
- association_foreign_key = foreign_key || "shopify_#{model_name.element.downcase}_id"
20
- association_primary_key = primary_key || :id
21
-
22
- # Store association metadata
23
- associations[name] = {
24
- type: :has_many,
25
- class_name: association_class_name,
26
- foreign_key: association_foreign_key,
27
- primary_key: association_primary_key
28
- }
29
-
30
- # Define the association method
31
- define_method name do
32
- return @_association_cache[name] if @_association_cache&.key?(name)
33
-
34
- @_association_cache ||= {}
35
-
36
- primary_key_value = send(association_primary_key)
37
- return @_association_cache[name] = [] if primary_key_value.blank?
38
-
39
- association_class = association_class_name.constantize
40
- @_association_cache[name] = association_class.where(association_foreign_key => primary_key_value)
41
- end
42
-
43
- # Define the association setter method for testing/mocking
44
- define_method "#{name}=" do |value|
45
- @_association_cache ||= {}
46
- @_association_cache[name] = value
47
- end
48
- end
49
-
50
- def has_one(name, class_name: nil, foreign_key: nil, primary_key: nil)
51
- association_class_name = class_name || name.to_s.classify
52
- association_foreign_key = foreign_key || "shopify_#{model_name.element.downcase}_id"
53
- association_primary_key = primary_key || :id
54
-
55
- # Store association metadata
56
- associations[name] = {
57
- type: :has_one,
58
- class_name: association_class_name,
59
- foreign_key: association_foreign_key,
60
- primary_key: association_primary_key
61
- }
62
-
63
- # Define the association method
64
- define_method name do
65
- return @_association_cache[name] if @_association_cache&.key?(name)
66
-
67
- @_association_cache ||= {}
68
-
69
- primary_key_value = send(association_primary_key)
70
- return @_association_cache[name] = nil if primary_key_value.blank?
71
-
72
- # Extract numeric ID from Shopify GID if needed
73
- if primary_key_value.is_a?(String)
74
- begin
75
- parsed_gid = URI::GID.parse(primary_key_value)
76
- primary_key_value = parsed_gid.model_id
77
- rescue URI::InvalidURIError, URI::BadURIError, ArgumentError
78
- # Not a GID, use value as-is
79
- end
80
- end
81
-
82
- association_class = association_class_name.constantize
83
- @_association_cache[name] = association_class.find_by(association_foreign_key => primary_key_value)
84
- end
85
-
86
- # Define the association setter method for testing/mocking
87
- define_method "#{name}=" do |value|
88
- @_association_cache ||= {}
89
- @_association_cache[name] = value
90
- end
91
- end
92
- end
93
- end
94
- end
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveShopifyGraphQL
4
- module Attributes
5
- extend ActiveSupport::Concern
6
-
7
- class_methods do
8
- # Define an attribute with automatic GraphQL path inference and type coercion.
9
- #
10
- # @param name [Symbol] The Ruby attribute name
11
- # @param path [String] The GraphQL field path (auto-inferred if not provided)
12
- # @param type [Symbol] The type for coercion (:string, :integer, :float, :boolean, :datetime)
13
- # @param null [Boolean] Whether the attribute can be null (default: true)
14
- # @param default [Object] Default value when GraphQL response is nil
15
- # @param transform [Proc] Custom transform block for the value
16
- # @param raw_graphql [String] Raw GraphQL string to inject directly (escape hatch for unsupported features)
17
- def attribute(name, path: nil, type: :string, null: true, default: nil, transform: nil, raw_graphql: nil)
18
- path ||= infer_path(name)
19
- config = { path: path, type: type, null: null, default: default, transform: transform, raw_graphql: raw_graphql }
20
-
21
- if @current_loader_context
22
- # Store in loader-specific context
23
- @loader_contexts[@current_loader_context][name] = config
24
- else
25
- # Store in base attributes
26
- @base_attributes ||= {}
27
- @base_attributes[name] = config
28
- end
29
-
30
- # Always create attr_accessor
31
- attr_accessor name unless method_defined?(name) || method_defined?("#{name}=")
32
- end
33
-
34
- # Get attributes for a specific loader class, merging base with loader-specific overrides.
35
- def attributes_for_loader(loader_class)
36
- base = @base_attributes || {}
37
- overrides = @loader_contexts&.dig(loader_class) || {}
38
-
39
- base.merge(overrides) { |_key, base_val, override_val| base_val.merge(override_val) }
40
- end
41
-
42
- private
43
-
44
- # Infer GraphQL path from Ruby attribute name (snake_case -> camelCase)
45
- def infer_path(name)
46
- name.to_s.gsub(/_([a-z])/) { ::Regexp.last_match(1).upcase }
47
- end
48
- end
49
- end
50
- end