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
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveShopifyGraphQL
|
|
4
|
-
# Builds GraphQL fragments from model attributes and connections.
|
|
5
|
-
class FragmentBuilder
|
|
6
|
-
def initialize(context)
|
|
7
|
-
@context = context
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
# Build a complete fragment node with all fields and connections
|
|
11
|
-
def build
|
|
12
|
-
raise NotImplementedError, "#{@context.loader_class} must define attributes" if @context.defined_attributes.empty?
|
|
13
|
-
|
|
14
|
-
fragment_node = QueryNode.new(
|
|
15
|
-
name: @context.fragment_name,
|
|
16
|
-
arguments: { on: @context.graphql_type },
|
|
17
|
-
node_type: :fragment
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
# Add field nodes from attributes
|
|
21
|
-
build_field_nodes.each { |node| fragment_node.add_child(node) }
|
|
22
|
-
|
|
23
|
-
# Add connection nodes
|
|
24
|
-
build_connection_nodes.each { |node| fragment_node.add_child(node) }
|
|
25
|
-
|
|
26
|
-
fragment_node
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Build field nodes from attribute definitions (protected for recursive calls)
|
|
30
|
-
def build_field_nodes
|
|
31
|
-
path_tree = {}
|
|
32
|
-
metafield_aliases = {}
|
|
33
|
-
raw_graphql_nodes = []
|
|
34
|
-
aliased_field_nodes = []
|
|
35
|
-
|
|
36
|
-
# Build a tree structure for nested paths
|
|
37
|
-
@context.defined_attributes.each do |attr_name, config|
|
|
38
|
-
if config[:raw_graphql]
|
|
39
|
-
raw_graphql_nodes << build_raw_graphql_node(attr_name, config[:raw_graphql])
|
|
40
|
-
elsif config[:is_metafield]
|
|
41
|
-
store_metafield_config(metafield_aliases, config)
|
|
42
|
-
else
|
|
43
|
-
path = config[:path]
|
|
44
|
-
if path.include?('.')
|
|
45
|
-
# Nested path - use tree structure (shared prefixes)
|
|
46
|
-
build_path_tree(path_tree, path)
|
|
47
|
-
else
|
|
48
|
-
# Simple path - add aliased field node
|
|
49
|
-
aliased_field_nodes << build_aliased_field_node(attr_name, path)
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Convert tree to QueryNode objects
|
|
55
|
-
nodes_from_tree(path_tree) + aliased_field_nodes + metafield_nodes(metafield_aliases) + raw_graphql_nodes
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Build QueryNode objects for all connections (protected for recursive calls)
|
|
59
|
-
def build_connection_nodes
|
|
60
|
-
return [] if @context.included_connections.empty?
|
|
61
|
-
|
|
62
|
-
connections = @context.connections
|
|
63
|
-
return [] if connections.empty?
|
|
64
|
-
|
|
65
|
-
normalized_includes = normalize_includes(@context.included_connections)
|
|
66
|
-
|
|
67
|
-
normalized_includes.filter_map do |connection_name, nested_includes|
|
|
68
|
-
connection_config = connections[connection_name]
|
|
69
|
-
next unless connection_config
|
|
70
|
-
|
|
71
|
-
build_connection_node(connection_config, nested_includes)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
private
|
|
76
|
-
|
|
77
|
-
def store_metafield_config(metafield_aliases, config)
|
|
78
|
-
alias_name = config[:metafield_alias]
|
|
79
|
-
value_field = config[:type] == :json ? 'jsonValue' : 'value'
|
|
80
|
-
|
|
81
|
-
metafield_aliases[alias_name] = {
|
|
82
|
-
namespace: config[:metafield_namespace],
|
|
83
|
-
key: config[:metafield_key],
|
|
84
|
-
value_field: value_field
|
|
85
|
-
}
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def build_raw_graphql_node(attr_name, raw_graphql)
|
|
89
|
-
# Prepend alias to raw GraphQL for predictable response mapping
|
|
90
|
-
aliased_raw_graphql = "#{attr_name}: #{raw_graphql}"
|
|
91
|
-
QueryNode.new(
|
|
92
|
-
name: "raw",
|
|
93
|
-
arguments: { raw_graphql: aliased_raw_graphql },
|
|
94
|
-
node_type: :raw
|
|
95
|
-
)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def build_aliased_field_node(attr_name, path)
|
|
99
|
-
alias_name = attr_name.to_s
|
|
100
|
-
# Only add alias if the attr_name differs from the GraphQL field name
|
|
101
|
-
alias_name = nil if alias_name == path
|
|
102
|
-
QueryNode.new(name: path, alias_name: alias_name, node_type: :field)
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def build_path_tree(path_tree, path)
|
|
106
|
-
path_parts = path.split('.')
|
|
107
|
-
current_level = path_tree
|
|
108
|
-
|
|
109
|
-
path_parts.each_with_index do |part, index|
|
|
110
|
-
if index == path_parts.length - 1
|
|
111
|
-
current_level[part] = true
|
|
112
|
-
else
|
|
113
|
-
current_level[part] ||= {}
|
|
114
|
-
current_level = current_level[part]
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def nodes_from_tree(tree)
|
|
120
|
-
tree.map do |key, value|
|
|
121
|
-
if value == true
|
|
122
|
-
QueryNode.new(name: key, node_type: :field)
|
|
123
|
-
else
|
|
124
|
-
children = nodes_from_tree(value)
|
|
125
|
-
QueryNode.new(name: key, node_type: :field, children: children)
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def metafield_nodes(metafield_aliases)
|
|
131
|
-
metafield_aliases.map do |alias_name, config|
|
|
132
|
-
value_node = QueryNode.new(name: config[:value_field], node_type: :field)
|
|
133
|
-
QueryNode.new(
|
|
134
|
-
name: "metafield",
|
|
135
|
-
alias_name: alias_name,
|
|
136
|
-
arguments: { namespace: config[:namespace], key: config[:key] },
|
|
137
|
-
node_type: :field,
|
|
138
|
-
children: [value_node]
|
|
139
|
-
)
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def build_connection_node(connection_config, nested_includes)
|
|
144
|
-
target_class = connection_config[:class_name].constantize
|
|
145
|
-
target_context = @context.for_model(target_class, new_connections: nested_includes)
|
|
146
|
-
|
|
147
|
-
# Build child nodes for the target model
|
|
148
|
-
child_nodes = build_target_field_nodes(target_context, nested_includes)
|
|
149
|
-
|
|
150
|
-
query_name = connection_config[:query_name]
|
|
151
|
-
original_name = connection_config[:original_name]
|
|
152
|
-
connection_type = connection_config[:type] || :connection
|
|
153
|
-
formatted_args = (connection_config[:default_arguments] || {}).transform_keys(&:to_sym)
|
|
154
|
-
|
|
155
|
-
# Add alias if the connection name differs from the query name
|
|
156
|
-
alias_name = original_name.to_s == query_name ? nil : original_name.to_s
|
|
157
|
-
|
|
158
|
-
node_type = connection_type == :singular ? :singular : :connection
|
|
159
|
-
QueryNode.new(
|
|
160
|
-
name: query_name,
|
|
161
|
-
alias_name: alias_name,
|
|
162
|
-
arguments: formatted_args,
|
|
163
|
-
node_type: node_type,
|
|
164
|
-
children: child_nodes
|
|
165
|
-
)
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def build_target_field_nodes(target_context, nested_includes)
|
|
169
|
-
# Build attribute nodes
|
|
170
|
-
attribute_nodes = if target_context.defined_attributes.any?
|
|
171
|
-
FragmentBuilder.new(target_context.with_connections([])).build_field_nodes
|
|
172
|
-
else
|
|
173
|
-
[QueryNode.new(name: "id", node_type: :field)]
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# Build nested connection nodes
|
|
177
|
-
return attribute_nodes if nested_includes.empty?
|
|
178
|
-
|
|
179
|
-
nested_builder = FragmentBuilder.new(target_context)
|
|
180
|
-
nested_connection_nodes = nested_builder.build_connection_nodes
|
|
181
|
-
attribute_nodes + nested_connection_nodes
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# Normalize includes from various formats to a consistent hash structure
|
|
185
|
-
def normalize_includes(includes)
|
|
186
|
-
includes = Array(includes)
|
|
187
|
-
includes.each_with_object({}) do |inc, normalized|
|
|
188
|
-
case inc
|
|
189
|
-
when Hash
|
|
190
|
-
inc.each do |key, value|
|
|
191
|
-
key = key.to_sym
|
|
192
|
-
normalized[key] ||= []
|
|
193
|
-
case value
|
|
194
|
-
when Hash then normalized[key] << value
|
|
195
|
-
when Array then normalized[key].concat(value)
|
|
196
|
-
else normalized[key] << value
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
when Symbol, String
|
|
200
|
-
normalized[inc.to_sym] ||= []
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
class << self
|
|
206
|
-
# Expose for external use (QueryTree needs this)
|
|
207
|
-
def normalize_includes(includes)
|
|
208
|
-
includes = Array(includes)
|
|
209
|
-
includes.each_with_object({}) do |inc, normalized|
|
|
210
|
-
case inc
|
|
211
|
-
when Hash
|
|
212
|
-
inc.each do |key, value|
|
|
213
|
-
key = key.to_sym
|
|
214
|
-
normalized[key] ||= []
|
|
215
|
-
case value
|
|
216
|
-
when Hash then normalized[key] << value
|
|
217
|
-
when Array then normalized[key].concat(value)
|
|
218
|
-
else normalized[key] << value
|
|
219
|
-
end
|
|
220
|
-
end
|
|
221
|
-
when Symbol, String
|
|
222
|
-
normalized[inc.to_sym] ||= []
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
end
|
|
228
|
-
end
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveShopifyGraphQL
|
|
4
|
-
# Centralizes GraphQL type resolution logic.
|
|
5
|
-
module GraphqlTypeResolver
|
|
6
|
-
extend ActiveSupport::Concern
|
|
7
|
-
|
|
8
|
-
class_methods do
|
|
9
|
-
# Set or get the base GraphQL type for this model.
|
|
10
|
-
#
|
|
11
|
-
# @param type [String, nil] The GraphQL type name to set, or nil to get
|
|
12
|
-
# @return [String] The GraphQL type name
|
|
13
|
-
# @raise [NotImplementedError] If no type is defined
|
|
14
|
-
def graphql_type(type = nil)
|
|
15
|
-
if type
|
|
16
|
-
if @current_loader_context
|
|
17
|
-
@loader_graphql_types ||= {}
|
|
18
|
-
@loader_graphql_types[@current_loader_context] = type
|
|
19
|
-
else
|
|
20
|
-
@base_graphql_type = type
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
@base_graphql_type || raise(NotImplementedError, "#{self} must define graphql_type")
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Get the GraphQL type for a specific loader class.
|
|
28
|
-
# Resolution order:
|
|
29
|
-
# 1. Loader-specific type defined via `for_loader`
|
|
30
|
-
# 2. Base graphql_type defined on the model
|
|
31
|
-
# 3. Type defined on the loader class itself
|
|
32
|
-
# 4. Inferred from model class name
|
|
33
|
-
#
|
|
34
|
-
# @param loader_class [Class] The loader class to resolve type for
|
|
35
|
-
# @return [String] The resolved GraphQL type
|
|
36
|
-
# @raise [NotImplementedError] If no type can be resolved
|
|
37
|
-
def graphql_type_for_loader(loader_class)
|
|
38
|
-
# 1. Check loader-specific override
|
|
39
|
-
return @loader_graphql_types[loader_class] if @loader_graphql_types&.key?(loader_class)
|
|
40
|
-
|
|
41
|
-
# 2. Check base graphql_type
|
|
42
|
-
return @base_graphql_type if @base_graphql_type
|
|
43
|
-
|
|
44
|
-
# 3. Check loader class itself
|
|
45
|
-
loader_type = loader_class.instance_variable_get(:@graphql_type)
|
|
46
|
-
return loader_type if loader_type
|
|
47
|
-
|
|
48
|
-
# 4. Infer from model name
|
|
49
|
-
return name.demodulize if respond_to?(:name) && name
|
|
50
|
-
|
|
51
|
-
raise NotImplementedError,
|
|
52
|
-
"#{self} must define graphql_type or #{loader_class} must define graphql_type"
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Resolve the GraphQL type from any source (model class, loader, or value).
|
|
56
|
-
# Useful for external callers that need to resolve type from various inputs.
|
|
57
|
-
#
|
|
58
|
-
# @param model_class [Class, nil] The model class
|
|
59
|
-
# @param loader_class [Class, nil] The loader class
|
|
60
|
-
# @return [String] The resolved GraphQL type
|
|
61
|
-
def resolve_graphql_type(model_class: nil, loader_class: nil)
|
|
62
|
-
if model_class.respond_to?(:graphql_type_for_loader) && loader_class
|
|
63
|
-
model_class.graphql_type_for_loader(loader_class)
|
|
64
|
-
elsif model_class.respond_to?(:graphql_type)
|
|
65
|
-
model_class.graphql_type
|
|
66
|
-
elsif loader_class.respond_to?(:graphql_type)
|
|
67
|
-
loader_class.graphql_type
|
|
68
|
-
elsif model_class.respond_to?(:name) && model_class.name
|
|
69
|
-
model_class.name.demodulize
|
|
70
|
-
else
|
|
71
|
-
raise ArgumentError, "Cannot resolve graphql_type from provided arguments"
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Module-level resolver for convenience
|
|
77
|
-
def self.resolve(model_class: nil, loader_class: nil)
|
|
78
|
-
if model_class.respond_to?(:graphql_type_for_loader) && loader_class
|
|
79
|
-
model_class.graphql_type_for_loader(loader_class)
|
|
80
|
-
elsif model_class.respond_to?(:graphql_type)
|
|
81
|
-
model_class.graphql_type
|
|
82
|
-
elsif loader_class.respond_to?(:graphql_type)
|
|
83
|
-
loader_class.graphql_type
|
|
84
|
-
elsif model_class.respond_to?(:name) && model_class.name
|
|
85
|
-
model_class.name.demodulize
|
|
86
|
-
else
|
|
87
|
-
raise ArgumentError, "Cannot resolve graphql_type"
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveShopifyGraphQL
|
|
4
|
-
# A scope object that holds included connections for eager loading.
|
|
5
|
-
# This allows chaining methods like find() and where() while maintaining
|
|
6
|
-
# the included connections configuration.
|
|
7
|
-
class IncludesScope
|
|
8
|
-
attr_reader :model_class, :included_connections
|
|
9
|
-
|
|
10
|
-
def initialize(model_class, included_connections)
|
|
11
|
-
@model_class = model_class
|
|
12
|
-
@included_connections = included_connections
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# Delegate find to the model class with a custom loader
|
|
16
|
-
def find(id, loader: nil)
|
|
17
|
-
loader ||= default_loader
|
|
18
|
-
@model_class.find(id, loader: loader)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# Delegate where to the model class with a custom loader
|
|
22
|
-
def where(*args, **options)
|
|
23
|
-
loader = options.delete(:loader) || default_loader
|
|
24
|
-
@model_class.where(*args, **options.merge(loader: loader))
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Delegate select to create a new scope with select
|
|
28
|
-
def select(*attributes)
|
|
29
|
-
selected_scope = @model_class.select(*attributes)
|
|
30
|
-
# Chain the includes on top of select
|
|
31
|
-
IncludesScope.new(selected_scope, @included_connections)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Allow chaining includes calls
|
|
35
|
-
def includes(*connection_names)
|
|
36
|
-
@model_class.includes(*(@included_connections + connection_names).uniq)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
private
|
|
40
|
-
|
|
41
|
-
def default_loader
|
|
42
|
-
@default_loader ||= @model_class.default_loader.class.new(
|
|
43
|
-
@model_class,
|
|
44
|
-
included_connections: @included_connections
|
|
45
|
-
)
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveShopifyGraphQL
|
|
4
|
-
# Provides capability to switch between different loaders within the same model
|
|
5
|
-
module LoaderSwitchable
|
|
6
|
-
extend ActiveSupport::Concern
|
|
7
|
-
|
|
8
|
-
# Generic method to execute with a specific loader
|
|
9
|
-
# @param loader_class [Class] The loader class to use
|
|
10
|
-
# @yield [Object] Block to execute with the loader
|
|
11
|
-
# @return [Object] Result of the block
|
|
12
|
-
def with_loader(loader_class, &_block)
|
|
13
|
-
old_loader = Thread.current[:active_shopify_graphql_loader]
|
|
14
|
-
Thread.current[:active_shopify_graphql_loader] = loader_class.new(self.class)
|
|
15
|
-
|
|
16
|
-
if block_given?
|
|
17
|
-
yield(self)
|
|
18
|
-
else
|
|
19
|
-
self
|
|
20
|
-
end
|
|
21
|
-
ensure
|
|
22
|
-
Thread.current[:active_shopify_graphql_loader] = old_loader
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Executes with the admin API loader
|
|
26
|
-
# @return [self]
|
|
27
|
-
def with_admin_api(&block)
|
|
28
|
-
with_loader(ActiveShopifyGraphQL::Loaders::AdminApiLoader, &block)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Executes with the customer account API loader
|
|
32
|
-
# @return [self]
|
|
33
|
-
def with_customer_account_api(&block)
|
|
34
|
-
with_loader(ActiveShopifyGraphQL::Loaders::CustomerAccountApiLoader, &block)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
class_methods do
|
|
38
|
-
# @!method use_loader(loader_class)
|
|
39
|
-
# Sets the default loader class for this model.
|
|
40
|
-
#
|
|
41
|
-
# @param loader_class [Class] The loader class to use as default
|
|
42
|
-
# @example
|
|
43
|
-
# class Customer < ActiveRecord::Base
|
|
44
|
-
# use_loader ActiveShopifyGraphQL::Loaders::CustomerAccountApiLoader
|
|
45
|
-
# end
|
|
46
|
-
def use_loader(loader_class)
|
|
47
|
-
@default_loader_class = loader_class
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Define loader-specific attribute and graphql_type overrides
|
|
51
|
-
# @param loader_class [Class] The loader class to override attributes for
|
|
52
|
-
def for_loader(loader_class, &block)
|
|
53
|
-
@current_loader_context = loader_class
|
|
54
|
-
@loader_contexts ||= {}
|
|
55
|
-
@loader_contexts[loader_class] ||= {}
|
|
56
|
-
instance_eval(&block) if block_given?
|
|
57
|
-
@current_loader_context = nil
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Class-level method to execute with admin API loader
|
|
61
|
-
# @return [LoaderProxy] Proxy object with find method
|
|
62
|
-
def with_admin_api
|
|
63
|
-
LoaderProxy.new(self, ActiveShopifyGraphQL::Loaders::AdminApiLoader.new(self))
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Class-level method to execute with customer account API loader
|
|
67
|
-
# @return [LoaderProxy] Proxy object with find method
|
|
68
|
-
def with_customer_account_api(token = nil)
|
|
69
|
-
LoaderProxy.new(self, ActiveShopifyGraphQL::Loaders::CustomerAccountApiLoader.new(self, token))
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
private
|
|
73
|
-
|
|
74
|
-
# Returns the default loader class (either set via DSL or inferred)
|
|
75
|
-
# @return [Class] The default loader class
|
|
76
|
-
def default_loader_class
|
|
77
|
-
@default_loader_class ||= ActiveShopifyGraphQL::Loaders::AdminApiLoader
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Simple proxy class to handle loader delegation
|
|
82
|
-
class LoaderProxy
|
|
83
|
-
def initialize(model_class, loader, included_connections: [], selected_attributes: nil)
|
|
84
|
-
@model_class = model_class
|
|
85
|
-
@loader = loader
|
|
86
|
-
@included_connections = included_connections
|
|
87
|
-
@selected_attributes = selected_attributes
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def includes(*connection_names)
|
|
91
|
-
# Validate connections exist
|
|
92
|
-
@model_class.send(:validate_includes_connections!, connection_names) if @model_class.respond_to?(:validate_includes_connections!, true)
|
|
93
|
-
|
|
94
|
-
# Collect connections with eager_load: true
|
|
95
|
-
auto_included_connections = @model_class.connections.select { |_name, config| config[:eager_load] }.keys
|
|
96
|
-
|
|
97
|
-
# Merge manual and automatic connections
|
|
98
|
-
all_included_connections = (@included_connections + connection_names + auto_included_connections).uniq
|
|
99
|
-
|
|
100
|
-
# Create a new loader with the included connections
|
|
101
|
-
new_loader = @loader.class.new(
|
|
102
|
-
@model_class,
|
|
103
|
-
*loader_extra_args,
|
|
104
|
-
selected_attributes: @selected_attributes,
|
|
105
|
-
included_connections: all_included_connections
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
LoaderProxy.new(
|
|
109
|
-
@model_class,
|
|
110
|
-
new_loader,
|
|
111
|
-
included_connections: all_included_connections,
|
|
112
|
-
selected_attributes: @selected_attributes
|
|
113
|
-
)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def select(*attribute_names)
|
|
117
|
-
new_selected = attribute_names.map(&:to_sym)
|
|
118
|
-
|
|
119
|
-
# Create a new loader with the selected attributes
|
|
120
|
-
new_loader = @loader.class.new(
|
|
121
|
-
@model_class,
|
|
122
|
-
*loader_extra_args,
|
|
123
|
-
selected_attributes: new_selected,
|
|
124
|
-
included_connections: @included_connections
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
LoaderProxy.new(
|
|
128
|
-
@model_class,
|
|
129
|
-
new_loader,
|
|
130
|
-
included_connections: @included_connections,
|
|
131
|
-
selected_attributes: new_selected
|
|
132
|
-
)
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def find(id = nil)
|
|
136
|
-
# For Customer Account API, if no ID is provided, load the current customer
|
|
137
|
-
if id.nil? && @loader.is_a?(ActiveShopifyGraphQL::Loaders::CustomerAccountApiLoader)
|
|
138
|
-
attributes = @loader.load_attributes
|
|
139
|
-
return nil if attributes.nil?
|
|
140
|
-
|
|
141
|
-
return @model_class.new(attributes)
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
# For other cases, require ID and use standard flow
|
|
145
|
-
return nil if id.nil?
|
|
146
|
-
|
|
147
|
-
gid = GidHelper.normalize_gid(id, @model_class.model_name.name.demodulize)
|
|
148
|
-
|
|
149
|
-
attributes = @loader.load_attributes(gid)
|
|
150
|
-
return nil if attributes.nil?
|
|
151
|
-
|
|
152
|
-
@model_class.new(attributes)
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# Delegate where to the model class with the specific loader
|
|
156
|
-
def where(*args, **options)
|
|
157
|
-
@model_class.where(*args, **options.merge(loader: @loader))
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
attr_reader :loader
|
|
161
|
-
|
|
162
|
-
def inspect
|
|
163
|
-
"#{@model_class.name}(with_#{@loader.class.name.demodulize})"
|
|
164
|
-
end
|
|
165
|
-
alias to_s inspect
|
|
166
|
-
|
|
167
|
-
private
|
|
168
|
-
|
|
169
|
-
# Returns extra arguments needed when creating a new loader of the same type
|
|
170
|
-
# For CustomerAccountApiLoader, this includes the token
|
|
171
|
-
def loader_extra_args
|
|
172
|
-
if @loader.is_a?(ActiveShopifyGraphQL::Loaders::CustomerAccountApiLoader)
|
|
173
|
-
[@loader.instance_variable_get(:@token)]
|
|
174
|
-
else
|
|
175
|
-
[]
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
end
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveShopifyGraphQL
|
|
4
|
-
module MetafieldAttributes
|
|
5
|
-
extend ActiveSupport::Concern
|
|
6
|
-
|
|
7
|
-
class_methods do
|
|
8
|
-
# Define a metafield attribute for this model.
|
|
9
|
-
#
|
|
10
|
-
# @param name [Symbol] The Ruby attribute name
|
|
11
|
-
# @param namespace [String] The metafield namespace
|
|
12
|
-
# @param key [String] The metafield key
|
|
13
|
-
# @param type [Symbol] The type for coercion (:string, :integer, :float, :boolean, :datetime, :json)
|
|
14
|
-
# @param null [Boolean] Whether the attribute can be null (default: true)
|
|
15
|
-
# @param default [Object] Default value when GraphQL response is nil
|
|
16
|
-
# @param transform [Proc] Custom transform block for the value
|
|
17
|
-
def metafield_attribute(name, namespace:, key:, type: :string, null: true, default: nil, transform: nil)
|
|
18
|
-
@metafields ||= {}
|
|
19
|
-
@metafields[name] = { namespace: namespace, key: key, type: type }
|
|
20
|
-
|
|
21
|
-
# Build metafield config
|
|
22
|
-
alias_name = "#{infer_path(name)}Metafield"
|
|
23
|
-
value_field = type == :json ? 'jsonValue' : 'value'
|
|
24
|
-
path = "#{alias_name}.#{value_field}"
|
|
25
|
-
|
|
26
|
-
config = {
|
|
27
|
-
path: path,
|
|
28
|
-
type: type,
|
|
29
|
-
null: null,
|
|
30
|
-
default: default,
|
|
31
|
-
transform: transform,
|
|
32
|
-
is_metafield: true,
|
|
33
|
-
metafield_alias: alias_name,
|
|
34
|
-
metafield_namespace: namespace,
|
|
35
|
-
metafield_key: key
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if @current_loader_context
|
|
39
|
-
@loader_contexts[@current_loader_context][name] = config
|
|
40
|
-
else
|
|
41
|
-
@base_attributes ||= {}
|
|
42
|
-
@base_attributes[name] = config
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
attr_accessor name unless method_defined?(name) || method_defined?("#{name}=")
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Get metafields defined for this model
|
|
49
|
-
def metafields
|
|
50
|
-
@metafields || {}
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
private
|
|
54
|
-
|
|
55
|
-
# Infer GraphQL path from Ruby attribute name (delegates to Attributes if available)
|
|
56
|
-
def infer_path(name)
|
|
57
|
-
name.to_s.gsub(/_([a-z])/) { ::Regexp.last_match(1).upcase }
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|