active_shopify_graphql 0.3.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 +4 -0
- data/README.md +187 -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/graphql_associations.rb +245 -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 +30 -28
- metadata +47 -15
- data/lib/active_shopify_graphql/associations.rb +0 -90
- 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 -159
- 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,159 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveShopifyGraphQL
|
|
4
|
-
module FinderMethods
|
|
5
|
-
extend ActiveSupport::Concern
|
|
6
|
-
|
|
7
|
-
class_methods do
|
|
8
|
-
# Find a single record by ID using the provided loader
|
|
9
|
-
# @param id [String, Integer] The record ID (will be converted to GID automatically)
|
|
10
|
-
# @param loader [ActiveShopifyGraphQL::Loader] The loader to use for fetching data
|
|
11
|
-
# @return [Object, nil] The model instance or nil if not found
|
|
12
|
-
def find(id, loader: default_loader)
|
|
13
|
-
gid = GidHelper.normalize_gid(id, model_name.name.demodulize)
|
|
14
|
-
|
|
15
|
-
# If we have included connections, we need to handle inverse_of properly
|
|
16
|
-
if loader.respond_to?(:load_with_instance) && loader.has_included_connections?
|
|
17
|
-
loader.load_with_instance(gid, self)
|
|
18
|
-
else
|
|
19
|
-
attributes = loader.load_attributes(gid)
|
|
20
|
-
return nil if attributes.nil?
|
|
21
|
-
|
|
22
|
-
new(attributes)
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Returns the default loader for this model's queries
|
|
27
|
-
# @return [ActiveGraphQL::Loader] The default loader instance
|
|
28
|
-
def default_loader
|
|
29
|
-
if respond_to?(:default_loader_instance)
|
|
30
|
-
default_loader_instance
|
|
31
|
-
else
|
|
32
|
-
@default_loader ||= begin
|
|
33
|
-
# Collect connections with eager_load: true
|
|
34
|
-
eagerly_loaded_connections = connections.select { |_name, config| config[:eager_load] }.keys
|
|
35
|
-
|
|
36
|
-
default_loader_class.new(
|
|
37
|
-
self,
|
|
38
|
-
included_connections: eagerly_loaded_connections
|
|
39
|
-
)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Allows setting a custom default loader (useful for testing)
|
|
45
|
-
# @param loader [ActiveGraphQL::Loader] The loader to set as default
|
|
46
|
-
def default_loader=(loader)
|
|
47
|
-
@default_loader = loader
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Select specific attributes to optimize GraphQL queries
|
|
51
|
-
# @param *attributes [Symbol] The attributes to select
|
|
52
|
-
# @return [Class] A class with modified default loader for method chaining
|
|
53
|
-
#
|
|
54
|
-
# @example
|
|
55
|
-
# Customer.select(:id, :email).find(123)
|
|
56
|
-
# Customer.select(:id, :email).where(first_name: "John")
|
|
57
|
-
def select(*attributes)
|
|
58
|
-
# Validate attributes exist
|
|
59
|
-
attrs = Array(attributes).flatten.map(&:to_sym)
|
|
60
|
-
validate_select_attributes!(attrs)
|
|
61
|
-
|
|
62
|
-
# Create a new class that inherits from self with a modified default loader
|
|
63
|
-
selected_class = Class.new(self)
|
|
64
|
-
|
|
65
|
-
# Override the default_loader method to return a loader with selected attributes
|
|
66
|
-
selected_class.define_singleton_method(:default_loader) do
|
|
67
|
-
@default_loader ||= superclass.default_loader.class.new(
|
|
68
|
-
superclass,
|
|
69
|
-
selected_attributes: attrs
|
|
70
|
-
)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Preserve the original class name and model name for GraphQL operations
|
|
74
|
-
selected_class.define_singleton_method(:name) { superclass.name }
|
|
75
|
-
selected_class.define_singleton_method(:model_name) { superclass.model_name }
|
|
76
|
-
|
|
77
|
-
selected_class
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Query for multiple records using attribute conditions
|
|
81
|
-
# @param conditions [Hash] The conditions to query (e.g., { email: "example@test.com", first_name: "John" })
|
|
82
|
-
# @param options [Hash] Options hash containing loader and limit (when first arg is a Hash)
|
|
83
|
-
# @option options [ActiveShopifyGraphQL::Loader] :loader The loader to use for fetching data
|
|
84
|
-
# @option options [Integer] :limit The maximum number of records to return (default: 250, max: 250)
|
|
85
|
-
# @return [Array<Object>] Array of model instances
|
|
86
|
-
# @raise [ArgumentError] If any attribute is not valid for querying
|
|
87
|
-
#
|
|
88
|
-
# @example
|
|
89
|
-
# # Keyword argument style (recommended)
|
|
90
|
-
# Customer.where(email: "john@example.com")
|
|
91
|
-
# Customer.where(first_name: "John", country: "Canada")
|
|
92
|
-
# Customer.where(orders_count: { gte: 5 })
|
|
93
|
-
# Customer.where(created_at: { gte: "2024-01-01", lt: "2024-02-01" })
|
|
94
|
-
#
|
|
95
|
-
# # Hash style with options
|
|
96
|
-
# Customer.where({ email: "john@example.com" }, loader: custom_loader, limit: 100)
|
|
97
|
-
def where(conditions_or_first_condition = {}, *args, **options)
|
|
98
|
-
# Handle both syntaxes:
|
|
99
|
-
# where(email: "john@example.com") - keyword args become options
|
|
100
|
-
# where({ email: "john@example.com" }, loader: custom_loader) - explicit hash + options
|
|
101
|
-
if conditions_or_first_condition.is_a?(Hash) && !conditions_or_first_condition.empty?
|
|
102
|
-
# Explicit hash provided as first argument
|
|
103
|
-
conditions = conditions_or_first_condition
|
|
104
|
-
# Any additional options passed as keyword args or second hash argument
|
|
105
|
-
final_options = args.first.is_a?(Hash) ? options.merge(args.first) : options
|
|
106
|
-
else
|
|
107
|
-
# Keyword arguments style - conditions come from options, excluding known option keys
|
|
108
|
-
known_option_keys = %i[loader limit]
|
|
109
|
-
conditions = options.except(*known_option_keys)
|
|
110
|
-
final_options = options.slice(*known_option_keys)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
loader = final_options[:loader] || default_loader
|
|
114
|
-
limit = final_options[:limit] || 250
|
|
115
|
-
|
|
116
|
-
# Ensure loader has model class set - needed for graphql_type inference
|
|
117
|
-
loader.instance_variable_set(:@model_class, self) if loader.instance_variable_get(:@model_class).nil?
|
|
118
|
-
|
|
119
|
-
attributes_array = loader.load_collection(conditions, limit: limit)
|
|
120
|
-
|
|
121
|
-
attributes_array.map { |attributes| new(attributes) }
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
private
|
|
125
|
-
|
|
126
|
-
# Validates that selected attributes exist in the model
|
|
127
|
-
# @param attributes [Array<Symbol>] The attributes to validate
|
|
128
|
-
# @raise [ArgumentError] If any attribute is invalid
|
|
129
|
-
def validate_select_attributes!(attributes)
|
|
130
|
-
return if attributes.empty?
|
|
131
|
-
|
|
132
|
-
available_attrs = available_select_attributes
|
|
133
|
-
invalid_attrs = attributes - available_attrs
|
|
134
|
-
|
|
135
|
-
return unless invalid_attrs.any?
|
|
136
|
-
|
|
137
|
-
raise ArgumentError, "Invalid attributes for #{name}: #{invalid_attrs.join(', ')}. " \
|
|
138
|
-
"Available attributes are: #{available_attrs.join(', ')}"
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# Gets all available attributes for selection
|
|
142
|
-
# @return [Array<Symbol>] Available attribute names
|
|
143
|
-
def available_select_attributes
|
|
144
|
-
attrs = []
|
|
145
|
-
|
|
146
|
-
# Get attributes from the model class
|
|
147
|
-
loader_class = default_loader.class
|
|
148
|
-
model_attrs = attributes_for_loader(loader_class)
|
|
149
|
-
attrs.concat(model_attrs.keys)
|
|
150
|
-
|
|
151
|
-
# Get attributes from the loader class
|
|
152
|
-
loader_attrs = default_loader.class.defined_attributes
|
|
153
|
-
attrs.concat(loader_attrs.keys)
|
|
154
|
-
|
|
155
|
-
attrs.map(&:to_sym).uniq.sort
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
end
|
|
@@ -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
|