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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/README.md +158 -56
- data/lib/active_shopify_graphql/configuration.rb +2 -15
- data/lib/active_shopify_graphql/connections/connection_loader.rb +141 -0
- data/lib/active_shopify_graphql/gid_helper.rb +2 -0
- data/lib/active_shopify_graphql/loader.rb +147 -126
- data/lib/active_shopify_graphql/loader_context.rb +2 -47
- data/lib/active_shopify_graphql/loader_proxy.rb +67 -0
- data/lib/active_shopify_graphql/loaders/admin_api_loader.rb +1 -17
- data/lib/active_shopify_graphql/loaders/customer_account_api_loader.rb +10 -26
- data/lib/active_shopify_graphql/model/associations.rb +94 -0
- data/lib/active_shopify_graphql/model/attributes.rb +48 -0
- data/lib/active_shopify_graphql/model/connections.rb +174 -0
- data/lib/active_shopify_graphql/model/finder_methods.rb +155 -0
- data/lib/active_shopify_graphql/model/graphql_type_resolver.rb +89 -0
- data/lib/active_shopify_graphql/model/loader_switchable.rb +79 -0
- data/lib/active_shopify_graphql/model/metafield_attributes.rb +59 -0
- data/lib/active_shopify_graphql/{base.rb → model.rb} +30 -16
- data/lib/active_shopify_graphql/model_builder.rb +53 -0
- data/lib/active_shopify_graphql/query/node/collection.rb +78 -0
- data/lib/active_shopify_graphql/query/node/connection.rb +16 -0
- data/lib/active_shopify_graphql/query/node/current_customer.rb +37 -0
- data/lib/active_shopify_graphql/query/node/field.rb +23 -0
- data/lib/active_shopify_graphql/query/node/fragment.rb +17 -0
- data/lib/active_shopify_graphql/query/node/nested_connection.rb +79 -0
- data/lib/active_shopify_graphql/query/node/raw.rb +15 -0
- data/lib/active_shopify_graphql/query/node/root_connection.rb +76 -0
- data/lib/active_shopify_graphql/query/node/single_record.rb +36 -0
- data/lib/active_shopify_graphql/query/node/singular.rb +16 -0
- data/lib/active_shopify_graphql/query/node.rb +95 -0
- data/lib/active_shopify_graphql/query/query_builder.rb +290 -0
- data/lib/active_shopify_graphql/query/relation.rb +424 -0
- data/lib/active_shopify_graphql/query/scope.rb +219 -0
- data/lib/active_shopify_graphql/response/page_info.rb +40 -0
- data/lib/active_shopify_graphql/response/paginated_result.rb +114 -0
- data/lib/active_shopify_graphql/response/response_mapper.rb +242 -0
- data/lib/active_shopify_graphql/search_query/hash_condition_formatter.rb +107 -0
- data/lib/active_shopify_graphql/search_query/parameter_binder.rb +68 -0
- data/lib/active_shopify_graphql/search_query/value_sanitizer.rb +24 -0
- data/lib/active_shopify_graphql/search_query.rb +34 -84
- data/lib/active_shopify_graphql/version.rb +1 -1
- data/lib/active_shopify_graphql.rb +29 -29
- metadata +46 -15
- data/lib/active_shopify_graphql/associations.rb +0 -94
- data/lib/active_shopify_graphql/attributes.rb +0 -50
- data/lib/active_shopify_graphql/connection_loader.rb +0 -96
- data/lib/active_shopify_graphql/connections.rb +0 -198
- data/lib/active_shopify_graphql/finder_methods.rb +0 -182
- data/lib/active_shopify_graphql/fragment_builder.rb +0 -228
- data/lib/active_shopify_graphql/graphql_type_resolver.rb +0 -91
- data/lib/active_shopify_graphql/includes_scope.rb +0 -48
- data/lib/active_shopify_graphql/loader_switchable.rb +0 -180
- data/lib/active_shopify_graphql/metafield_attributes.rb +0 -61
- data/lib/active_shopify_graphql/query_node.rb +0 -173
- data/lib/active_shopify_graphql/query_tree.rb +0 -225
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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,37 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
7
|
-
require
|
|
8
|
-
require
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
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/
|
|
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/
|
|
91
|
-
- lib/active_shopify_graphql/
|
|
92
|
-
- lib/active_shopify_graphql/
|
|
93
|
-
- lib/active_shopify_graphql/
|
|
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
|