active_shopify_graphql 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/lint.yml +35 -0
  3. data/.github/workflows/test.yml +35 -0
  4. data/.rubocop.yml +50 -0
  5. data/AGENTS.md +53 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +544 -0
  8. data/Rakefile +8 -0
  9. data/lib/active_shopify_graphql/associations.rb +90 -0
  10. data/lib/active_shopify_graphql/attributes.rb +49 -0
  11. data/lib/active_shopify_graphql/base.rb +29 -0
  12. data/lib/active_shopify_graphql/configuration.rb +29 -0
  13. data/lib/active_shopify_graphql/connection_loader.rb +96 -0
  14. data/lib/active_shopify_graphql/connections/connection_proxy.rb +112 -0
  15. data/lib/active_shopify_graphql/connections.rb +170 -0
  16. data/lib/active_shopify_graphql/finder_methods.rb +154 -0
  17. data/lib/active_shopify_graphql/fragment_builder.rb +195 -0
  18. data/lib/active_shopify_graphql/gid_helper.rb +54 -0
  19. data/lib/active_shopify_graphql/graphql_type_resolver.rb +91 -0
  20. data/lib/active_shopify_graphql/loader.rb +183 -0
  21. data/lib/active_shopify_graphql/loader_context.rb +88 -0
  22. data/lib/active_shopify_graphql/loader_switchable.rb +121 -0
  23. data/lib/active_shopify_graphql/loaders/admin_api_loader.rb +32 -0
  24. data/lib/active_shopify_graphql/loaders/customer_account_api_loader.rb +71 -0
  25. data/lib/active_shopify_graphql/metafield_attributes.rb +61 -0
  26. data/lib/active_shopify_graphql/query_node.rb +160 -0
  27. data/lib/active_shopify_graphql/query_tree.rb +204 -0
  28. data/lib/active_shopify_graphql/response_mapper.rb +202 -0
  29. data/lib/active_shopify_graphql/search_query.rb +71 -0
  30. data/lib/active_shopify_graphql/version.rb +5 -0
  31. data/lib/active_shopify_graphql.rb +34 -0
  32. metadata +147 -0
@@ -0,0 +1,121 @@
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)
84
+ @model_class = model_class
85
+ @loader = loader
86
+ end
87
+
88
+ def find(id = nil)
89
+ # For Customer Account API, if no ID is provided, load the current customer
90
+ if id.nil? && @loader.is_a?(ActiveShopifyGraphQL::Loaders::CustomerAccountApiLoader)
91
+ attributes = @loader.load_attributes
92
+ return nil if attributes.nil?
93
+
94
+ return @model_class.new(attributes)
95
+ end
96
+
97
+ # For other cases, require ID and use standard flow
98
+ return nil if id.nil?
99
+
100
+ gid = GidHelper.normalize_gid(id, @model_class.model_name.name.demodulize)
101
+
102
+ attributes = @loader.load_attributes(gid)
103
+ return nil if attributes.nil?
104
+
105
+ @model_class.new(attributes)
106
+ end
107
+
108
+ # Delegate where to the model class with the specific loader
109
+ def where(*args, **options)
110
+ @model_class.where(*args, **options.merge(loader: @loader))
111
+ end
112
+
113
+ attr_reader :loader
114
+
115
+ def inspect
116
+ "#{@model_class.name}(with_#{@loader.class.name.demodulize})"
117
+ end
118
+ alias to_s inspect
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveShopifyGraphQL
4
+ module Loaders
5
+ class AdminApiLoader < Loader
6
+ def initialize(model_class = nil, selected_attributes: nil, included_connections: nil)
7
+ super(model_class, selected_attributes: selected_attributes, included_connections: included_connections)
8
+ end
9
+
10
+ def perform_graphql_query(query, **variables)
11
+ log_query(query, variables) if should_log?
12
+
13
+ client = ActiveShopifyGraphQL.configuration.admin_api_client
14
+ raise Error, "Admin API client not configured. Please configure it using ActiveShopifyGraphQL.configure" unless client
15
+
16
+ client.execute(query, **variables)
17
+ end
18
+
19
+ private
20
+
21
+ def should_log?
22
+ ActiveShopifyGraphQL.configuration.log_queries && ActiveShopifyGraphQL.configuration.logger
23
+ end
24
+
25
+ def log_query(query, variables)
26
+ logger = ActiveShopifyGraphQL.configuration.logger
27
+ logger.info("ActiveShopifyGraphQL Query (Admin API):\n#{query}")
28
+ logger.info("ActiveShopifyGraphQL Variables:\n#{variables}")
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveShopifyGraphQL
4
+ module Loaders
5
+ class CustomerAccountApiLoader < Loader
6
+ def initialize(model_class = nil, token = nil, selected_attributes: nil, included_connections: nil)
7
+ super(model_class, selected_attributes: selected_attributes, included_connections: included_connections)
8
+ @token = token
9
+ end
10
+
11
+ # Override to handle Customer queries that don't need an ID
12
+ def graphql_query(model_type = nil)
13
+ type = model_type || graphql_type
14
+ if type == 'Customer'
15
+ customer_only_query(type)
16
+ else
17
+ super(type)
18
+ end
19
+ end
20
+
21
+ # Override load_attributes to handle the Customer case
22
+ def load_attributes(id = nil)
23
+ type = graphql_type
24
+ query = graphql_query(type)
25
+
26
+ variables = type == 'Customer' ? {} : { id: id }
27
+ response_data = perform_graphql_query(query, **variables)
28
+
29
+ return nil if response_data.nil?
30
+
31
+ map_response_to_attributes(response_data)
32
+ end
33
+
34
+ def client
35
+ client_class = ActiveShopifyGraphQL.configuration.customer_account_client_class
36
+ raise Error, "Customer Account API client class not configured" unless client_class
37
+
38
+ @client ||= client_class.from_config(@token)
39
+ end
40
+
41
+ def perform_graphql_query(query, **variables)
42
+ log_query(query, variables) if should_log?
43
+ client.query(query, variables)
44
+ end
45
+
46
+ private
47
+
48
+ def should_log?
49
+ ActiveShopifyGraphQL.configuration.log_queries && ActiveShopifyGraphQL.configuration.logger
50
+ end
51
+
52
+ def log_query(query, variables)
53
+ logger = ActiveShopifyGraphQL.configuration.logger
54
+ logger.info("ActiveShopifyGraphQL Query (Customer Account API):\n#{query}")
55
+ logger.info("ActiveShopifyGraphQL Variables:\n#{variables}")
56
+ end
57
+
58
+ def customer_only_query(model_type = nil)
59
+ type = model_type || graphql_type
60
+ compact = ActiveShopifyGraphQL.configuration.compact_queries
61
+ frag = fragment
62
+
63
+ if compact
64
+ "#{frag} query getCurrentCustomer { #{query_name(type)} { ...#{fragment_name(type)} } }"
65
+ else
66
+ "#{frag}\n\nquery getCurrentCustomer {\n #{query_name(type)} {\n ...#{fragment_name(type)}\n }\n}\n"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,61 @@
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
@@ -0,0 +1,160 @@
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
+ else
44
+ raise ArgumentError, "Unknown node type: #{@node_type}"
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def compact?
51
+ ActiveShopifyGraphQL.configuration.compact_queries
52
+ end
53
+
54
+ def render_field(indent_level:)
55
+ # Simple field with no children
56
+ field_name = @alias_name ? "#{@alias_name}: #{@name}" : @name
57
+ args_string = format_arguments
58
+ full_name = "#{field_name}#{args_string}"
59
+
60
+ return full_name unless has_children?
61
+
62
+ # Field with nested structure (e.g., defaultAddress { city })
63
+ indent = compact? ? "" : " " * indent_level
64
+ nested_indent = compact? ? "" : " " * (indent_level + 1)
65
+
66
+ nested_fields = @children.map { |child| child.to_s(indent_level: indent_level + 1) }
67
+
68
+ if compact?
69
+ "#{full_name} { #{nested_fields.join(' ')} }"
70
+ else
71
+ separator = "\n"
72
+ "#{full_name} {#{separator}#{nested_indent}#{nested_fields.join("\n#{nested_indent}")}#{separator}#{indent}}"
73
+ end
74
+ end
75
+
76
+ def render_connection(indent_level:)
77
+ args_string = format_arguments
78
+
79
+ indent = compact? ? "" : " " * indent_level
80
+ nested_indent = compact? ? "" : " " * (indent_level + 1)
81
+
82
+ # Build nested fields from children
83
+ nested_fields = @children.map { |child| child.to_s(indent_level: indent_level + 2) }
84
+ fields_string = nested_fields.join(compact? ? " " : "\n#{nested_indent} ")
85
+
86
+ if compact?
87
+ "#{@name}#{args_string} { edges { node { #{fields_string} } } }"
88
+ else
89
+ <<~GRAPHQL.strip
90
+ #{@name}#{args_string} {
91
+ #{nested_indent}edges {
92
+ #{nested_indent} node {
93
+ #{nested_indent} #{fields_string}
94
+ #{nested_indent} }
95
+ #{nested_indent}}
96
+ #{indent}}
97
+ GRAPHQL
98
+ end
99
+ end
100
+
101
+ def render_singular(indent_level:)
102
+ args_string = format_arguments
103
+
104
+ indent = compact? ? "" : " " * indent_level
105
+ nested_indent = compact? ? "" : " " * (indent_level + 1)
106
+
107
+ # Build nested fields from children
108
+ nested_fields = @children.map { |child| child.to_s(indent_level: indent_level + 1) }
109
+ fields_string = nested_fields.join(compact? ? " " : "\n#{nested_indent}")
110
+
111
+ if compact?
112
+ "#{@name}#{args_string} { #{fields_string} }"
113
+ else
114
+ "#{@name}#{args_string} {\n#{nested_indent}#{fields_string}\n#{indent}}"
115
+ end
116
+ end
117
+
118
+ def render_fragment
119
+ # Fragment fields are the children
120
+ fields = @children.map { |child| child.to_s(indent_level: 0) }
121
+ all_fields = fields.join(compact? ? " " : "\n")
122
+
123
+ if compact?
124
+ "fragment #{@name} on #{@arguments[:on]} { #{all_fields} }"
125
+ else
126
+ "fragment #{@name} on #{@arguments[:on]} {\n#{all_fields}\n}"
127
+ end
128
+ end
129
+
130
+ def format_arguments
131
+ return "" if @arguments.empty?
132
+
133
+ args = @arguments.map do |key, value|
134
+ # Convert Ruby snake_case to GraphQL camelCase
135
+ graphql_key = key.to_s.camelize(:lower)
136
+
137
+ # Format value based on type
138
+ formatted_value = case value
139
+ when String
140
+ # Check if it needs quotes (query parameter vs enum values)
141
+ # For metafields and most strings, add quotes
142
+ if %i[namespace key].include?(key.to_sym)
143
+ "\"#{value}\""
144
+ elsif key.to_sym == :query
145
+ "\"#{value}\""
146
+ else
147
+ value
148
+ end
149
+ when Symbol
150
+ value.to_s
151
+ else
152
+ value
153
+ end
154
+
155
+ "#{graphql_key}: #{formatted_value}"
156
+ end
157
+
158
+ "(#{args.join(', ')})"
159
+ end
160
+ end
@@ -0,0 +1,204 @@
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
+ def self.build_collection_query(context, query_name:, variables:, connection_type: :nodes_only)
31
+ new(context).tap do |tree|
32
+ tree.add_fragment(FragmentBuilder.new(context).build)
33
+ tree.set_query_config(
34
+ type: :collection,
35
+ model_type: context.graphql_type,
36
+ query_name: query_name,
37
+ fragment_name: context.fragment_name,
38
+ variables: variables,
39
+ connection_type: connection_type
40
+ )
41
+ end.to_s
42
+ end
43
+
44
+ def self.build_connection_query(context, query_name:, variables:, parent_query: nil, connection_type: :connection)
45
+ new(context).tap do |tree|
46
+ tree.add_fragment(FragmentBuilder.new(context).build)
47
+ tree.set_query_config(
48
+ type: :connection,
49
+ query_name: query_name,
50
+ fragment_name: context.fragment_name,
51
+ variables: variables,
52
+ parent_query: parent_query,
53
+ connection_type: connection_type
54
+ )
55
+ end.to_s
56
+ end
57
+
58
+ # Delegate normalize_includes to FragmentBuilder
59
+ def self.normalize_includes(includes)
60
+ FragmentBuilder.normalize_includes(includes)
61
+ end
62
+
63
+ # Helper methods (kept for backward compatibility)
64
+ def self.query_name(graphql_type)
65
+ graphql_type.downcase
66
+ end
67
+
68
+ def self.fragment_name(graphql_type)
69
+ "#{graphql_type}Fragment"
70
+ end
71
+
72
+ def add_fragment(fragment_node)
73
+ @fragments << fragment_node
74
+ end
75
+
76
+ def set_query_config(config)
77
+ @query_config = config
78
+ end
79
+
80
+ def to_s
81
+ case @query_config[:type]
82
+ when :single_record then render_single_record_query
83
+ when :collection then render_collection_query
84
+ when :connection then render_connection_query
85
+ else ""
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def compact?
92
+ ActiveShopifyGraphQL.configuration.compact_queries
93
+ end
94
+
95
+ def fragments_string
96
+ @fragments.map(&:to_s).join(compact? ? " " : "\n\n")
97
+ end
98
+
99
+ def field_signature(variables)
100
+ params = build_field_parameters(variables.compact)
101
+ params.empty? ? "" : "(#{params.join(', ')})"
102
+ end
103
+
104
+ def render_single_record_query
105
+ type = @query_config[:model_type]
106
+ query_name = @query_config[:query_name]
107
+ fragment_name = @query_config[:fragment_name]
108
+
109
+ if compact?
110
+ "#{fragments_string} query get#{type}($id: ID!) { #{query_name}(id: $id) { ...#{fragment_name} } }"
111
+ else
112
+ "#{fragments_string}\n\nquery get#{type}($id: ID!) {\n #{query_name}(id: $id) {\n ...#{fragment_name}\n }\n}\n"
113
+ end
114
+ end
115
+
116
+ def render_collection_query
117
+ type = @query_config[:model_type]
118
+ query_name = @query_config[:query_name]
119
+ fragment_name = @query_config[:fragment_name]
120
+ variables = @query_config[:variables] || {}
121
+ connection_type = @query_config[:connection_type] || :nodes_only
122
+
123
+ field_sig = field_signature(variables)
124
+
125
+ if compact?
126
+ body = wrap_connection_body_compact(fragment_name, connection_type)
127
+ "#{fragments_string} query get#{type.pluralize} { #{query_name}#{field_sig} { #{body} } }"
128
+ else
129
+ body = wrap_connection_body_formatted(fragment_name, connection_type, 2)
130
+ "#{fragments_string}\nquery get#{type.pluralize} {\n #{query_name}#{field_sig} {\n#{body}\n }\n}\n"
131
+ end
132
+ end
133
+
134
+ def render_connection_query
135
+ query_name = @query_config[:query_name]
136
+ fragment_name = @query_config[:fragment_name]
137
+ variables = @query_config[:variables] || {}
138
+ parent_query = @query_config[:parent_query]
139
+ connection_type = @query_config[:connection_type] || :connection
140
+
141
+ field_sig = field_signature(variables)
142
+
143
+ if parent_query
144
+ render_nested_connection_query(query_name, fragment_name, field_sig, parent_query, connection_type)
145
+ else
146
+ render_root_connection_query(query_name, fragment_name, field_sig, connection_type)
147
+ end
148
+ end
149
+
150
+ def render_nested_connection_query(query_name, fragment_name, field_sig, parent_query, connection_type)
151
+ if compact?
152
+ body = wrap_connection_body_compact(fragment_name, connection_type)
153
+ "#{fragments_string} query($id: ID!) { #{parent_query} { #{query_name}#{field_sig} { #{body} } } }"
154
+ else
155
+ body = wrap_connection_body_formatted(fragment_name, connection_type, 3)
156
+ "#{fragments_string}\nquery($id: ID!) {\n #{parent_query} {\n #{query_name}#{field_sig} {\n#{body}\n }\n }\n}\n"
157
+ end
158
+ end
159
+
160
+ def render_root_connection_query(query_name, fragment_name, field_sig, connection_type)
161
+ if compact?
162
+ body = wrap_connection_body_compact(fragment_name, connection_type)
163
+ "#{fragments_string} query { #{query_name}#{field_sig} { #{body} } }"
164
+ else
165
+ body = wrap_connection_body_formatted(fragment_name, connection_type, 2)
166
+ "#{fragments_string}\nquery {\n #{query_name}#{field_sig} {\n#{body}\n }\n}\n"
167
+ end
168
+ end
169
+
170
+ def wrap_connection_body_compact(fragment_name, connection_type)
171
+ case connection_type
172
+ when :singular then "...#{fragment_name}"
173
+ when :nodes_only then "nodes { ...#{fragment_name} }"
174
+ else "edges { node { ...#{fragment_name} } }"
175
+ end
176
+ end
177
+
178
+ def wrap_connection_body_formatted(fragment_name, connection_type, indent_level)
179
+ indent = " " * indent_level
180
+ case connection_type
181
+ when :singular
182
+ "#{indent}...#{fragment_name}"
183
+ when :nodes_only
184
+ "#{indent}nodes {\n#{indent} ...#{fragment_name}\n#{indent}}"
185
+ else
186
+ "#{indent}edges {\n#{indent} node {\n#{indent} ...#{fragment_name}\n#{indent} }\n#{indent}}"
187
+ end
188
+ end
189
+
190
+ def build_field_parameters(variables)
191
+ variables.map do |key, value|
192
+ "#{key.to_s.camelize(:lower)}: #{format_inline_value(key, value)}"
193
+ end
194
+ end
195
+
196
+ def format_inline_value(key, value)
197
+ case value
198
+ when Integer, TrueClass, FalseClass then value.to_s
199
+ when String then key.to_sym == :query ? "\"#{value}\"" : value
200
+ else value.to_s
201
+ end
202
+ end
203
+ end
204
+ end