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,173 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Represents a node in the GraphQL query tree
|
|
4
|
-
# This allows building the complete query structure before converting to a string
|
|
5
|
-
class QueryNode
|
|
6
|
-
attr_reader :name, :arguments, :children, :node_type, :alias_name
|
|
7
|
-
|
|
8
|
-
# @param name [String] The field name (e.g., 'id', 'displayName', 'orders')
|
|
9
|
-
# @param alias_name [String] Optional field alias (e.g., 'myAlias' for 'myAlias: fieldName')
|
|
10
|
-
# @param arguments [Hash] Field arguments (e.g., { first: 10, sortKey: 'CREATED_AT' })
|
|
11
|
-
# @param node_type [Symbol] Type of node: :field, :connection, :singular, :fragment
|
|
12
|
-
# @param children [Array<QueryNode>] Child nodes for nested structures
|
|
13
|
-
def initialize(name:, alias_name: nil, arguments: {}, node_type: :field, children: [])
|
|
14
|
-
@name = name
|
|
15
|
-
@alias_name = alias_name
|
|
16
|
-
@arguments = arguments
|
|
17
|
-
@node_type = node_type
|
|
18
|
-
@children = children
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# Add a child node
|
|
22
|
-
def add_child(node)
|
|
23
|
-
@children << node
|
|
24
|
-
node
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Check if node has children
|
|
28
|
-
def has_children?
|
|
29
|
-
@children.any?
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Convert node to GraphQL string
|
|
33
|
-
def to_s(indent_level: 0)
|
|
34
|
-
case @node_type
|
|
35
|
-
when :field
|
|
36
|
-
render_field(indent_level: indent_level)
|
|
37
|
-
when :connection
|
|
38
|
-
render_connection(indent_level: indent_level)
|
|
39
|
-
when :singular
|
|
40
|
-
render_singular(indent_level: indent_level)
|
|
41
|
-
when :fragment
|
|
42
|
-
render_fragment
|
|
43
|
-
when :raw
|
|
44
|
-
render_raw
|
|
45
|
-
else
|
|
46
|
-
raise ArgumentError, "Unknown node type: #{@node_type}"
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
def compact?
|
|
53
|
-
ActiveShopifyGraphQL.configuration.compact_queries
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def render_field(indent_level:)
|
|
57
|
-
# Simple field with no children
|
|
58
|
-
field_name = @alias_name ? "#{@alias_name}: #{@name}" : @name
|
|
59
|
-
args_string = format_arguments
|
|
60
|
-
full_name = "#{field_name}#{args_string}"
|
|
61
|
-
|
|
62
|
-
return full_name unless has_children?
|
|
63
|
-
|
|
64
|
-
# Field with nested structure (e.g., defaultAddress { city })
|
|
65
|
-
indent = compact? ? "" : " " * indent_level
|
|
66
|
-
nested_indent = compact? ? "" : " " * (indent_level + 1)
|
|
67
|
-
|
|
68
|
-
nested_fields = @children.map { |child| child.to_s(indent_level: indent_level + 1) }
|
|
69
|
-
|
|
70
|
-
if compact?
|
|
71
|
-
"#{full_name} { #{nested_fields.join(' ')} }"
|
|
72
|
-
else
|
|
73
|
-
separator = "\n"
|
|
74
|
-
"#{full_name} {#{separator}#{nested_indent}#{nested_fields.join("\n#{nested_indent}")}#{separator}#{indent}}"
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def render_connection(indent_level:)
|
|
79
|
-
args_string = format_arguments
|
|
80
|
-
|
|
81
|
-
indent = compact? ? "" : " " * indent_level
|
|
82
|
-
nested_indent = compact? ? "" : " " * (indent_level + 1)
|
|
83
|
-
|
|
84
|
-
# Build nested fields from children
|
|
85
|
-
nested_fields = @children.map { |child| child.to_s(indent_level: indent_level + 2) }
|
|
86
|
-
fields_string = nested_fields.join(compact? ? " " : "\n#{nested_indent} ")
|
|
87
|
-
|
|
88
|
-
# Include alias if present
|
|
89
|
-
field_name = @alias_name ? "#{@alias_name}: #{@name}" : @name
|
|
90
|
-
|
|
91
|
-
if compact?
|
|
92
|
-
"#{field_name}#{args_string} { edges { node { #{fields_string} } } }"
|
|
93
|
-
else
|
|
94
|
-
<<~GRAPHQL.strip
|
|
95
|
-
#{field_name}#{args_string} {
|
|
96
|
-
#{nested_indent}edges {
|
|
97
|
-
#{nested_indent} node {
|
|
98
|
-
#{nested_indent} #{fields_string}
|
|
99
|
-
#{nested_indent} }
|
|
100
|
-
#{nested_indent}}
|
|
101
|
-
#{indent}}
|
|
102
|
-
GRAPHQL
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def render_singular(indent_level:)
|
|
107
|
-
args_string = format_arguments
|
|
108
|
-
|
|
109
|
-
indent = compact? ? "" : " " * indent_level
|
|
110
|
-
nested_indent = compact? ? "" : " " * (indent_level + 1)
|
|
111
|
-
|
|
112
|
-
# Build nested fields from children
|
|
113
|
-
nested_fields = @children.map { |child| child.to_s(indent_level: indent_level + 1) }
|
|
114
|
-
fields_string = nested_fields.join(compact? ? " " : "\n#{nested_indent}")
|
|
115
|
-
|
|
116
|
-
# Include alias if present
|
|
117
|
-
field_name = @alias_name ? "#{@alias_name}: #{@name}" : @name
|
|
118
|
-
|
|
119
|
-
if compact?
|
|
120
|
-
"#{field_name}#{args_string} { #{fields_string} }"
|
|
121
|
-
else
|
|
122
|
-
"#{field_name}#{args_string} {\n#{nested_indent}#{fields_string}\n#{indent}}"
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def render_fragment
|
|
127
|
-
# Fragment fields are the children
|
|
128
|
-
fields = @children.map { |child| child.to_s(indent_level: 0) }
|
|
129
|
-
all_fields = fields.join(compact? ? " " : "\n")
|
|
130
|
-
|
|
131
|
-
if compact?
|
|
132
|
-
"fragment #{@name} on #{@arguments[:on]} { #{all_fields} }"
|
|
133
|
-
else
|
|
134
|
-
"fragment #{@name} on #{@arguments[:on]} {\n#{all_fields}\n}"
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def render_raw
|
|
139
|
-
# Raw GraphQL string stored in arguments[:raw_graphql]
|
|
140
|
-
@arguments[:raw_graphql]
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def format_arguments
|
|
144
|
-
return "" if @arguments.empty?
|
|
145
|
-
|
|
146
|
-
args = @arguments.map do |key, value|
|
|
147
|
-
# Convert Ruby snake_case to GraphQL camelCase
|
|
148
|
-
graphql_key = key.to_s.camelize(:lower)
|
|
149
|
-
|
|
150
|
-
# Format value based on type
|
|
151
|
-
formatted_value = case value
|
|
152
|
-
when String
|
|
153
|
-
# Check if it needs quotes (query parameter vs enum values)
|
|
154
|
-
# For metafields and most strings, add quotes
|
|
155
|
-
if %i[namespace key].include?(key.to_sym)
|
|
156
|
-
"\"#{value}\""
|
|
157
|
-
elsif key.to_sym == :query
|
|
158
|
-
"\"#{value}\""
|
|
159
|
-
else
|
|
160
|
-
value
|
|
161
|
-
end
|
|
162
|
-
when Symbol
|
|
163
|
-
value.to_s
|
|
164
|
-
else
|
|
165
|
-
value
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
"#{graphql_key}: #{formatted_value}"
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
"(#{args.join(', ')})"
|
|
172
|
-
end
|
|
173
|
-
end
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveShopifyGraphQL
|
|
4
|
-
# Builds complete GraphQL queries from a LoaderContext.
|
|
5
|
-
# Refactored for Single Responsibility - only handles query string generation.
|
|
6
|
-
# Fragment building is delegated to FragmentBuilder.
|
|
7
|
-
class QueryTree
|
|
8
|
-
attr_reader :context
|
|
9
|
-
|
|
10
|
-
def initialize(context)
|
|
11
|
-
@context = context
|
|
12
|
-
@fragments = []
|
|
13
|
-
@query_config = {}
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# Class-level factory methods for building complete queries
|
|
17
|
-
|
|
18
|
-
def self.build_single_record_query(context)
|
|
19
|
-
new(context).tap do |tree|
|
|
20
|
-
tree.add_fragment(FragmentBuilder.new(context).build)
|
|
21
|
-
tree.set_query_config(
|
|
22
|
-
type: :single_record,
|
|
23
|
-
model_type: context.graphql_type,
|
|
24
|
-
query_name: context.query_name,
|
|
25
|
-
fragment_name: context.fragment_name
|
|
26
|
-
)
|
|
27
|
-
end.to_s
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Build a query that doesn't require an ID parameter (e.g., Customer Account API's current customer)
|
|
31
|
-
def self.build_current_customer_query(context, query_name: nil)
|
|
32
|
-
new(context).tap do |tree|
|
|
33
|
-
tree.add_fragment(FragmentBuilder.new(context).build)
|
|
34
|
-
tree.set_query_config(
|
|
35
|
-
type: :current_customer,
|
|
36
|
-
model_type: context.graphql_type,
|
|
37
|
-
query_name: query_name || context.query_name,
|
|
38
|
-
fragment_name: context.fragment_name
|
|
39
|
-
)
|
|
40
|
-
end.to_s
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def self.build_collection_query(context, query_name:, variables:, connection_type: :nodes_only)
|
|
44
|
-
new(context).tap do |tree|
|
|
45
|
-
tree.add_fragment(FragmentBuilder.new(context).build)
|
|
46
|
-
tree.set_query_config(
|
|
47
|
-
type: :collection,
|
|
48
|
-
model_type: context.graphql_type,
|
|
49
|
-
query_name: query_name,
|
|
50
|
-
fragment_name: context.fragment_name,
|
|
51
|
-
variables: variables,
|
|
52
|
-
connection_type: connection_type
|
|
53
|
-
)
|
|
54
|
-
end.to_s
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def self.build_connection_query(context, query_name:, variables:, parent_query: nil, connection_type: :connection)
|
|
58
|
-
new(context).tap do |tree|
|
|
59
|
-
tree.add_fragment(FragmentBuilder.new(context).build)
|
|
60
|
-
tree.set_query_config(
|
|
61
|
-
type: :connection,
|
|
62
|
-
query_name: query_name,
|
|
63
|
-
fragment_name: context.fragment_name,
|
|
64
|
-
variables: variables,
|
|
65
|
-
parent_query: parent_query,
|
|
66
|
-
connection_type: connection_type
|
|
67
|
-
)
|
|
68
|
-
end.to_s
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Delegate normalize_includes to FragmentBuilder
|
|
72
|
-
def self.normalize_includes(includes)
|
|
73
|
-
FragmentBuilder.normalize_includes(includes)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def self.fragment_name(graphql_type)
|
|
77
|
-
"#{graphql_type}Fragment"
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def add_fragment(fragment_node)
|
|
81
|
-
@fragments << fragment_node
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def set_query_config(config)
|
|
85
|
-
@query_config = config
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def to_s
|
|
89
|
-
case @query_config[:type]
|
|
90
|
-
when :single_record then render_single_record_query
|
|
91
|
-
when :current_customer then render_current_customer_query
|
|
92
|
-
when :collection then render_collection_query
|
|
93
|
-
when :connection then render_connection_query
|
|
94
|
-
else ""
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
private
|
|
99
|
-
|
|
100
|
-
def compact?
|
|
101
|
-
ActiveShopifyGraphQL.configuration.compact_queries
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def fragments_string
|
|
105
|
-
@fragments.map(&:to_s).join(compact? ? " " : "\n\n")
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def field_signature(variables)
|
|
109
|
-
params = build_field_parameters(variables.compact)
|
|
110
|
-
params.empty? ? "" : "(#{params.join(', ')})"
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def render_single_record_query
|
|
114
|
-
type = @query_config[:model_type]
|
|
115
|
-
query_name = @query_config[:query_name]
|
|
116
|
-
fragment_name = @query_config[:fragment_name]
|
|
117
|
-
|
|
118
|
-
if compact?
|
|
119
|
-
"#{fragments_string} query get#{type}($id: ID!) { #{query_name}(id: $id) { ...#{fragment_name} } }"
|
|
120
|
-
else
|
|
121
|
-
"#{fragments_string}\n\nquery get#{type}($id: ID!) {\n #{query_name}(id: $id) {\n ...#{fragment_name}\n }\n}\n"
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def render_current_customer_query
|
|
126
|
-
type = @query_config[:model_type]
|
|
127
|
-
query_name = @query_config[:query_name]
|
|
128
|
-
fragment_name = @query_config[:fragment_name]
|
|
129
|
-
|
|
130
|
-
if compact?
|
|
131
|
-
"#{fragments_string} query getCurrent#{type} { #{query_name} { ...#{fragment_name} } }"
|
|
132
|
-
else
|
|
133
|
-
"#{fragments_string}\n\nquery getCurrent#{type} {\n #{query_name} {\n ...#{fragment_name}\n }\n}\n"
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def render_collection_query
|
|
138
|
-
type = @query_config[:model_type]
|
|
139
|
-
query_name = @query_config[:query_name]
|
|
140
|
-
fragment_name = @query_config[:fragment_name]
|
|
141
|
-
variables = @query_config[:variables] || {}
|
|
142
|
-
connection_type = @query_config[:connection_type] || :nodes_only
|
|
143
|
-
|
|
144
|
-
field_sig = field_signature(variables)
|
|
145
|
-
|
|
146
|
-
if compact?
|
|
147
|
-
body = wrap_connection_body_compact(fragment_name, connection_type)
|
|
148
|
-
"#{fragments_string} query get#{type.pluralize} { #{query_name}#{field_sig} { #{body} } }"
|
|
149
|
-
else
|
|
150
|
-
body = wrap_connection_body_formatted(fragment_name, connection_type, 2)
|
|
151
|
-
"#{fragments_string}\nquery get#{type.pluralize} {\n #{query_name}#{field_sig} {\n#{body}\n }\n}\n"
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def render_connection_query
|
|
156
|
-
query_name = @query_config[:query_name]
|
|
157
|
-
fragment_name = @query_config[:fragment_name]
|
|
158
|
-
variables = @query_config[:variables] || {}
|
|
159
|
-
parent_query = @query_config[:parent_query]
|
|
160
|
-
connection_type = @query_config[:connection_type] || :connection
|
|
161
|
-
|
|
162
|
-
field_sig = field_signature(variables)
|
|
163
|
-
|
|
164
|
-
if parent_query
|
|
165
|
-
render_nested_connection_query(query_name, fragment_name, field_sig, parent_query, connection_type)
|
|
166
|
-
else
|
|
167
|
-
render_root_connection_query(query_name, fragment_name, field_sig, connection_type)
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def render_nested_connection_query(query_name, fragment_name, field_sig, parent_query, connection_type)
|
|
172
|
-
if compact?
|
|
173
|
-
body = wrap_connection_body_compact(fragment_name, connection_type)
|
|
174
|
-
"#{fragments_string} query($id: ID!) { #{parent_query} { #{query_name}#{field_sig} { #{body} } } }"
|
|
175
|
-
else
|
|
176
|
-
body = wrap_connection_body_formatted(fragment_name, connection_type, 3)
|
|
177
|
-
"#{fragments_string}\nquery($id: ID!) {\n #{parent_query} {\n #{query_name}#{field_sig} {\n#{body}\n }\n }\n}\n"
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def render_root_connection_query(query_name, fragment_name, field_sig, connection_type)
|
|
182
|
-
if compact?
|
|
183
|
-
body = wrap_connection_body_compact(fragment_name, connection_type)
|
|
184
|
-
"#{fragments_string} query { #{query_name}#{field_sig} { #{body} } }"
|
|
185
|
-
else
|
|
186
|
-
body = wrap_connection_body_formatted(fragment_name, connection_type, 2)
|
|
187
|
-
"#{fragments_string}\nquery {\n #{query_name}#{field_sig} {\n#{body}\n }\n}\n"
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def wrap_connection_body_compact(fragment_name, connection_type)
|
|
192
|
-
case connection_type
|
|
193
|
-
when :singular then "...#{fragment_name}"
|
|
194
|
-
when :nodes_only then "nodes { ...#{fragment_name} }"
|
|
195
|
-
else "edges { node { ...#{fragment_name} } }"
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def wrap_connection_body_formatted(fragment_name, connection_type, indent_level)
|
|
200
|
-
indent = " " * indent_level
|
|
201
|
-
case connection_type
|
|
202
|
-
when :singular
|
|
203
|
-
"#{indent}...#{fragment_name}"
|
|
204
|
-
when :nodes_only
|
|
205
|
-
"#{indent}nodes {\n#{indent} ...#{fragment_name}\n#{indent}}"
|
|
206
|
-
else
|
|
207
|
-
"#{indent}edges {\n#{indent} node {\n#{indent} ...#{fragment_name}\n#{indent} }\n#{indent}}"
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def build_field_parameters(variables)
|
|
212
|
-
variables.map do |key, value|
|
|
213
|
-
"#{key.to_s.camelize(:lower)}: #{format_inline_value(key, value)}"
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def format_inline_value(key, value)
|
|
218
|
-
case value
|
|
219
|
-
when Integer, TrueClass, FalseClass then value.to_s
|
|
220
|
-
when String then key.to_sym == :query ? "\"#{value}\"" : value
|
|
221
|
-
else value.to_s
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
end
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveShopifyGraphQL
|
|
4
|
-
# Handles mapping GraphQL responses to model attributes.
|
|
5
|
-
# Refactored to use LoaderContext and unified mapping methods.
|
|
6
|
-
class ResponseMapper
|
|
7
|
-
attr_reader :context
|
|
8
|
-
|
|
9
|
-
def initialize(context)
|
|
10
|
-
@context = context
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# Map GraphQL response to attributes using declared attribute metadata
|
|
14
|
-
# @param response_data [Hash] The full GraphQL response
|
|
15
|
-
# @param root_path [Array<String>] Path to the data root (e.g., ["data", "customer"])
|
|
16
|
-
# @return [Hash] Mapped attributes
|
|
17
|
-
def map_response(response_data, root_path: nil)
|
|
18
|
-
root_path ||= ["data", @context.query_name]
|
|
19
|
-
root_data = response_data.dig(*root_path)
|
|
20
|
-
return {} unless root_data
|
|
21
|
-
|
|
22
|
-
map_node_to_attributes(root_data)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Map a single node's data to attributes (used for both root and nested)
|
|
26
|
-
def map_node_to_attributes(node_data)
|
|
27
|
-
return {} unless node_data
|
|
28
|
-
|
|
29
|
-
result = {}
|
|
30
|
-
@context.defined_attributes.each do |attr_name, config|
|
|
31
|
-
value = extract_and_transform_value(node_data, config, attr_name)
|
|
32
|
-
result[attr_name] = value
|
|
33
|
-
end
|
|
34
|
-
result
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Extract connection data from GraphQL response for eager loading
|
|
38
|
-
def extract_connection_data(response_data, root_path: nil, parent_instance: nil)
|
|
39
|
-
return {} if @context.included_connections.empty?
|
|
40
|
-
|
|
41
|
-
root_path ||= ["data", @context.query_name]
|
|
42
|
-
root_data = response_data.dig(*root_path)
|
|
43
|
-
return {} unless root_data
|
|
44
|
-
|
|
45
|
-
extract_connections_from_node(root_data, parent_instance)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Extract connections from a node (reusable for nested connections)
|
|
49
|
-
def extract_connections_from_node(node_data, parent_instance = nil)
|
|
50
|
-
return {} if @context.included_connections.empty?
|
|
51
|
-
|
|
52
|
-
connections = @context.connections
|
|
53
|
-
return {} if connections.empty?
|
|
54
|
-
|
|
55
|
-
normalized_includes = FragmentBuilder.normalize_includes(@context.included_connections)
|
|
56
|
-
connection_cache = {}
|
|
57
|
-
|
|
58
|
-
normalized_includes.each do |connection_name, nested_includes|
|
|
59
|
-
connection_config = connections[connection_name]
|
|
60
|
-
next unless connection_config
|
|
61
|
-
|
|
62
|
-
records = extract_connection_records(node_data, connection_config, nested_includes, parent_instance: parent_instance)
|
|
63
|
-
connection_cache[connection_name] = records if records
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
connection_cache
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Map nested connection response (when loading via parent query)
|
|
70
|
-
def map_nested_connection_response(response_data, connection_field_name, parent, connection_config = nil)
|
|
71
|
-
parent_type = parent.class.graphql_type_for_loader(@context.loader_class)
|
|
72
|
-
parent_query_name = parent_type.camelize(:lower)
|
|
73
|
-
connection_type = connection_config&.dig(:type) || :connection
|
|
74
|
-
|
|
75
|
-
if connection_type == :singular
|
|
76
|
-
node_data = response_data.dig("data", parent_query_name, connection_field_name)
|
|
77
|
-
return nil unless node_data
|
|
78
|
-
|
|
79
|
-
build_model_instance(node_data)
|
|
80
|
-
else
|
|
81
|
-
edges = response_data.dig("data", parent_query_name, connection_field_name, "edges")
|
|
82
|
-
return [] unless edges
|
|
83
|
-
|
|
84
|
-
edges.filter_map do |edge|
|
|
85
|
-
node_data = edge["node"]
|
|
86
|
-
build_model_instance(node_data) if node_data
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# Map root connection response
|
|
92
|
-
def map_connection_response(response_data, query_name, connection_config = nil)
|
|
93
|
-
connection_type = connection_config&.dig(:type) || :connection
|
|
94
|
-
|
|
95
|
-
if connection_type == :singular
|
|
96
|
-
node_data = response_data.dig("data", query_name)
|
|
97
|
-
return nil unless node_data
|
|
98
|
-
|
|
99
|
-
build_model_instance(node_data)
|
|
100
|
-
else
|
|
101
|
-
edges = response_data.dig("data", query_name, "edges")
|
|
102
|
-
return [] unless edges
|
|
103
|
-
|
|
104
|
-
edges.filter_map do |edge|
|
|
105
|
-
node_data = edge["node"]
|
|
106
|
-
build_model_instance(node_data) if node_data
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
private
|
|
112
|
-
|
|
113
|
-
def extract_and_transform_value(node_data, config, attr_name)
|
|
114
|
-
path = config[:path]
|
|
115
|
-
|
|
116
|
-
value = if config[:raw_graphql]
|
|
117
|
-
# For raw_graphql, the alias is the attr_name, then dig using path if nested
|
|
118
|
-
raw_data = node_data[attr_name.to_s]
|
|
119
|
-
if path.include?('.')
|
|
120
|
-
# Path is relative to the aliased root
|
|
121
|
-
path_parts = path.split('.')[1..] # Skip the first part (attr_name itself)
|
|
122
|
-
path_parts.any? ? raw_data&.dig(*path_parts) : raw_data
|
|
123
|
-
else
|
|
124
|
-
raw_data
|
|
125
|
-
end
|
|
126
|
-
elsif path.include?('.')
|
|
127
|
-
# Nested path - dig using the full path
|
|
128
|
-
path_parts = path.split('.')
|
|
129
|
-
node_data.dig(*path_parts)
|
|
130
|
-
else
|
|
131
|
-
# Simple path - use attr_name as key (matches the alias in the query)
|
|
132
|
-
node_data[attr_name.to_s]
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
value = apply_defaults_and_transforms(value, config)
|
|
136
|
-
validate_null_constraint!(value, config, attr_name)
|
|
137
|
-
coerce_value(value, config[:type])
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def apply_defaults_and_transforms(value, config)
|
|
141
|
-
if value.nil?
|
|
142
|
-
return config[:default] unless config[:default].nil?
|
|
143
|
-
|
|
144
|
-
return config[:transform]&.call(value)
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
config[:transform] ? config[:transform].call(value) : value
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def validate_null_constraint!(value, config, attr_name)
|
|
151
|
-
return unless !config[:null] && value.nil?
|
|
152
|
-
|
|
153
|
-
raise ArgumentError, "Attribute '#{attr_name}' (GraphQL path: '#{config[:path]}') cannot be null but received nil"
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def coerce_value(value, type)
|
|
157
|
-
return nil if value.nil?
|
|
158
|
-
return value if value.is_a?(Array) # Preserve arrays
|
|
159
|
-
|
|
160
|
-
type_caster(type).cast(value)
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def type_caster(type)
|
|
164
|
-
case type
|
|
165
|
-
when :string then ActiveModel::Type::String.new
|
|
166
|
-
when :integer then ActiveModel::Type::Integer.new
|
|
167
|
-
when :float then ActiveModel::Type::Float.new
|
|
168
|
-
when :boolean then ActiveModel::Type::Boolean.new
|
|
169
|
-
when :datetime then ActiveModel::Type::DateTime.new
|
|
170
|
-
else ActiveModel::Type::Value.new
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def extract_connection_records(node_data, connection_config, nested_includes, parent_instance: nil)
|
|
175
|
-
# Use original_name (Ruby attr name) as the response key since we alias connections
|
|
176
|
-
response_key = connection_config[:original_name].to_s
|
|
177
|
-
connection_type = connection_config[:type] || :connection
|
|
178
|
-
target_class = connection_config[:class_name].constantize
|
|
179
|
-
connection_name = connection_config[:original_name]
|
|
180
|
-
|
|
181
|
-
if connection_type == :singular
|
|
182
|
-
item_data = node_data[response_key]
|
|
183
|
-
return nil unless item_data
|
|
184
|
-
|
|
185
|
-
build_nested_model_instance(item_data, target_class, nested_includes,
|
|
186
|
-
parent_instance: parent_instance,
|
|
187
|
-
parent_connection_name: connection_name,
|
|
188
|
-
connection_config: connection_config)
|
|
189
|
-
else
|
|
190
|
-
edges = node_data.dig(response_key, "edges")
|
|
191
|
-
return nil unless edges
|
|
192
|
-
|
|
193
|
-
edges.filter_map do |edge|
|
|
194
|
-
item_data = edge["node"]
|
|
195
|
-
if item_data
|
|
196
|
-
build_nested_model_instance(item_data, target_class, nested_includes,
|
|
197
|
-
parent_instance: parent_instance,
|
|
198
|
-
parent_connection_name: connection_name,
|
|
199
|
-
connection_config: connection_config)
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def build_model_instance(node_data)
|
|
206
|
-
return nil unless node_data
|
|
207
|
-
|
|
208
|
-
attributes = map_node_to_attributes(node_data)
|
|
209
|
-
@context.model_class.new(attributes)
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
def build_nested_model_instance(node_data, target_class, nested_includes, parent_instance: nil, parent_connection_name: nil, connection_config: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
213
|
-
nested_context = @context.for_model(target_class, new_connections: nested_includes)
|
|
214
|
-
nested_mapper = ResponseMapper.new(nested_context)
|
|
215
|
-
|
|
216
|
-
attributes = nested_mapper.map_node_to_attributes(node_data)
|
|
217
|
-
instance = target_class.new(attributes)
|
|
218
|
-
|
|
219
|
-
# Populate inverse cache if inverse_of is specified
|
|
220
|
-
if parent_instance && connection_config && connection_config[:inverse_of]
|
|
221
|
-
inverse_name = connection_config[:inverse_of]
|
|
222
|
-
instance.instance_variable_set(:@_connection_cache, {}) unless instance.instance_variable_get(:@_connection_cache)
|
|
223
|
-
cache = instance.instance_variable_get(:@_connection_cache)
|
|
224
|
-
|
|
225
|
-
# Check the type of the inverse connection to determine how to cache
|
|
226
|
-
if target_class.respond_to?(:connections) && target_class.connections[inverse_name]
|
|
227
|
-
inverse_type = target_class.connections[inverse_name][:type]
|
|
228
|
-
cache[inverse_name] =
|
|
229
|
-
if inverse_type == :singular
|
|
230
|
-
parent_instance
|
|
231
|
-
else
|
|
232
|
-
# For collection inverses, wrap parent in an array
|
|
233
|
-
[parent_instance]
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
# Handle nested connections recursively (instance becomes parent for its children)
|
|
239
|
-
if nested_includes.any?
|
|
240
|
-
nested_data = nested_mapper.extract_connections_from_node(node_data, instance)
|
|
241
|
-
nested_data.each do |nested_name, nested_records|
|
|
242
|
-
instance.send("#{nested_name}=", nested_records)
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
instance
|
|
247
|
-
end
|
|
248
|
-
end
|
|
249
|
-
end
|