active_shopify_graphql 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/README.md +158 -56
- data/lib/active_shopify_graphql/configuration.rb +2 -15
- data/lib/active_shopify_graphql/connections/connection_loader.rb +141 -0
- data/lib/active_shopify_graphql/gid_helper.rb +2 -0
- data/lib/active_shopify_graphql/loader.rb +147 -126
- data/lib/active_shopify_graphql/loader_context.rb +2 -47
- data/lib/active_shopify_graphql/loader_proxy.rb +67 -0
- data/lib/active_shopify_graphql/loaders/admin_api_loader.rb +1 -17
- data/lib/active_shopify_graphql/loaders/customer_account_api_loader.rb +10 -26
- data/lib/active_shopify_graphql/model/associations.rb +94 -0
- data/lib/active_shopify_graphql/model/attributes.rb +48 -0
- data/lib/active_shopify_graphql/model/connections.rb +174 -0
- data/lib/active_shopify_graphql/model/finder_methods.rb +155 -0
- data/lib/active_shopify_graphql/model/graphql_type_resolver.rb +89 -0
- data/lib/active_shopify_graphql/model/loader_switchable.rb +79 -0
- data/lib/active_shopify_graphql/model/metafield_attributes.rb +59 -0
- data/lib/active_shopify_graphql/{base.rb → model.rb} +30 -16
- data/lib/active_shopify_graphql/model_builder.rb +53 -0
- data/lib/active_shopify_graphql/query/node/collection.rb +78 -0
- data/lib/active_shopify_graphql/query/node/connection.rb +16 -0
- data/lib/active_shopify_graphql/query/node/current_customer.rb +37 -0
- data/lib/active_shopify_graphql/query/node/field.rb +23 -0
- data/lib/active_shopify_graphql/query/node/fragment.rb +17 -0
- data/lib/active_shopify_graphql/query/node/nested_connection.rb +79 -0
- data/lib/active_shopify_graphql/query/node/raw.rb +15 -0
- data/lib/active_shopify_graphql/query/node/root_connection.rb +76 -0
- data/lib/active_shopify_graphql/query/node/single_record.rb +36 -0
- data/lib/active_shopify_graphql/query/node/singular.rb +16 -0
- data/lib/active_shopify_graphql/query/node.rb +95 -0
- data/lib/active_shopify_graphql/query/query_builder.rb +290 -0
- data/lib/active_shopify_graphql/query/relation.rb +424 -0
- data/lib/active_shopify_graphql/query/scope.rb +219 -0
- data/lib/active_shopify_graphql/response/page_info.rb +40 -0
- data/lib/active_shopify_graphql/response/paginated_result.rb +114 -0
- data/lib/active_shopify_graphql/response/response_mapper.rb +242 -0
- data/lib/active_shopify_graphql/search_query/hash_condition_formatter.rb +107 -0
- data/lib/active_shopify_graphql/search_query/parameter_binder.rb +68 -0
- data/lib/active_shopify_graphql/search_query/value_sanitizer.rb +24 -0
- data/lib/active_shopify_graphql/search_query.rb +34 -84
- data/lib/active_shopify_graphql/version.rb +1 -1
- data/lib/active_shopify_graphql.rb +29 -29
- metadata +46 -15
- data/lib/active_shopify_graphql/associations.rb +0 -94
- data/lib/active_shopify_graphql/attributes.rb +0 -50
- data/lib/active_shopify_graphql/connection_loader.rb +0 -96
- data/lib/active_shopify_graphql/connections.rb +0 -198
- data/lib/active_shopify_graphql/finder_methods.rb +0 -182
- data/lib/active_shopify_graphql/fragment_builder.rb +0 -228
- data/lib/active_shopify_graphql/graphql_type_resolver.rb +0 -91
- data/lib/active_shopify_graphql/includes_scope.rb +0 -48
- data/lib/active_shopify_graphql/loader_switchable.rb +0 -180
- data/lib/active_shopify_graphql/metafield_attributes.rb +0 -61
- data/lib/active_shopify_graphql/query_node.rb +0 -173
- data/lib/active_shopify_graphql/query_tree.rb +0 -225
- data/lib/active_shopify_graphql/response_mapper.rb +0 -249
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveShopifyGraphQL
|
|
4
|
+
# Pure factory for building model instances from attribute hashes.
|
|
5
|
+
# Separates data mapping (Loader/ResponseMapper) from object construction.
|
|
6
|
+
#
|
|
7
|
+
# Responsibilities:
|
|
8
|
+
# - Build single model instances from attributes
|
|
9
|
+
# - Apply connection caches to instances
|
|
10
|
+
# - Handle batch building with connection caching
|
|
11
|
+
#
|
|
12
|
+
# @example Single instance
|
|
13
|
+
# attributes = loader.load_attributes(gid)
|
|
14
|
+
# customer = ModelBuilder.build(Customer, attributes)
|
|
15
|
+
#
|
|
16
|
+
# @example Batch building
|
|
17
|
+
# attributes_list = loader.load_paginated_attributes(...)
|
|
18
|
+
# customers = ModelBuilder.build_many(Customer, attributes_list)
|
|
19
|
+
class ModelBuilder
|
|
20
|
+
class << self
|
|
21
|
+
# Build a single model instance from attributes
|
|
22
|
+
# @param model_class [Class] The model class to instantiate
|
|
23
|
+
# @param attributes [Hash] Attribute hash (may contain :_connection_cache)
|
|
24
|
+
# @return [Object] The instantiated model with cached connections
|
|
25
|
+
def build(model_class, attributes)
|
|
26
|
+
return nil if attributes.nil?
|
|
27
|
+
|
|
28
|
+
instance = model_class.new(attributes)
|
|
29
|
+
apply_connection_cache(instance, attributes)
|
|
30
|
+
instance
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Build multiple model instances from an array of attribute hashes
|
|
34
|
+
# @param model_class [Class] The model class to instantiate
|
|
35
|
+
# @param attributes_array [Array<Hash>] Array of attribute hashes
|
|
36
|
+
# @return [Array<Object>] Array of instantiated models
|
|
37
|
+
def build_many(model_class, attributes_array)
|
|
38
|
+
return [] if attributes_array.nil? || attributes_array.empty?
|
|
39
|
+
|
|
40
|
+
attributes_array.filter_map { |attrs| build(model_class, attrs) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Apply connection cache from attributes to an already-instantiated model
|
|
44
|
+
# @param instance [Object] The model instance
|
|
45
|
+
# @param attributes [Hash] Attribute hash that may contain :_connection_cache
|
|
46
|
+
def apply_connection_cache(instance, attributes)
|
|
47
|
+
return unless attributes.is_a?(Hash) && attributes.key?(:_connection_cache)
|
|
48
|
+
|
|
49
|
+
instance.instance_variable_set(:@_connection_cache, attributes[:_connection_cache])
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveShopifyGraphQL
|
|
4
|
+
module Query
|
|
5
|
+
class Node
|
|
6
|
+
# Renders a collection query with optional pagination.
|
|
7
|
+
# Example without pagination:
|
|
8
|
+
# query getCustomers {
|
|
9
|
+
# customers(first: 10) {
|
|
10
|
+
# nodes {
|
|
11
|
+
# ...CustomerFragment
|
|
12
|
+
# }
|
|
13
|
+
# }
|
|
14
|
+
# }
|
|
15
|
+
#
|
|
16
|
+
# Example with pagination:
|
|
17
|
+
# query getCustomers {
|
|
18
|
+
# customers(first: 10, after: "cursor") {
|
|
19
|
+
# pageInfo {
|
|
20
|
+
# hasNextPage
|
|
21
|
+
# hasPreviousPage
|
|
22
|
+
# startCursor
|
|
23
|
+
# endCursor
|
|
24
|
+
# }
|
|
25
|
+
# nodes {
|
|
26
|
+
# ...CustomerFragment
|
|
27
|
+
# }
|
|
28
|
+
# }
|
|
29
|
+
# }
|
|
30
|
+
class Collection < Node
|
|
31
|
+
attr_reader :model_type, :query_name, :fragment_name, :variables, :fragments, :include_page_info
|
|
32
|
+
|
|
33
|
+
def initialize(model_type:, query_name:, fragment_name:, variables: {}, fragments: [], include_page_info: false)
|
|
34
|
+
@model_type = model_type
|
|
35
|
+
@query_name = query_name
|
|
36
|
+
@fragment_name = fragment_name
|
|
37
|
+
@variables = variables
|
|
38
|
+
@fragments = fragments
|
|
39
|
+
@include_page_info = include_page_info
|
|
40
|
+
super(name: query_name)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_s
|
|
44
|
+
parts = [fragments_string, "query get#{model_type.pluralize} { #{query_name}#{field_signature} {"]
|
|
45
|
+
parts << PAGE_INFO_FIELDS if @include_page_info
|
|
46
|
+
parts << "nodes { ...#{fragment_name} }"
|
|
47
|
+
parts << "} }"
|
|
48
|
+
parts.join(' ')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def fragments_string
|
|
54
|
+
@fragments.map(&:to_s).join(' ')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def field_signature
|
|
58
|
+
params = build_field_parameters(@variables.compact)
|
|
59
|
+
params.empty? ? "" : "(#{params.join(', ')})"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def build_field_parameters(variables)
|
|
63
|
+
variables.map do |key, value|
|
|
64
|
+
"#{key.to_s.camelize(:lower)}: #{format_inline_value(key, value)}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def format_inline_value(key, value)
|
|
69
|
+
case value
|
|
70
|
+
when Integer, TrueClass, FalseClass then value.to_s
|
|
71
|
+
when String then STRING_KEYS_NEEDING_QUOTES.include?(key.to_sym) ? "\"#{value}\"" : value
|
|
72
|
+
else value.to_s
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveShopifyGraphQL
|
|
4
|
+
module Query
|
|
5
|
+
class Node
|
|
6
|
+
# Renders connection fields using the nodes pattern.
|
|
7
|
+
# Example: `orders(first: 10) { nodes { id createdAt } }`
|
|
8
|
+
class Connection < Node
|
|
9
|
+
def to_s
|
|
10
|
+
nested_fields = render_children
|
|
11
|
+
"#{field_name_with_alias}#{format_arguments} { nodes { #{nested_fields.join(' ')} } }"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveShopifyGraphQL
|
|
4
|
+
module Query
|
|
5
|
+
class Node
|
|
6
|
+
# Renders a query that doesn't require an ID parameter.
|
|
7
|
+
# Used for Customer Account API's current customer query.
|
|
8
|
+
# Example:
|
|
9
|
+
# query getCurrentCustomer {
|
|
10
|
+
# customer {
|
|
11
|
+
# ...CustomerFragment
|
|
12
|
+
# }
|
|
13
|
+
# }
|
|
14
|
+
class CurrentCustomer < Node
|
|
15
|
+
attr_reader :model_type, :query_name, :fragment_name, :fragments
|
|
16
|
+
|
|
17
|
+
def initialize(model_type:, query_name:, fragment_name:, fragments: [])
|
|
18
|
+
@model_type = model_type
|
|
19
|
+
@query_name = query_name
|
|
20
|
+
@fragment_name = fragment_name
|
|
21
|
+
@fragments = fragments
|
|
22
|
+
super(name: query_name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_s
|
|
26
|
+
"#{fragments_string} query getCurrent#{model_type} { #{query_name} { ...#{fragment_name} } }"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def fragments_string
|
|
32
|
+
@fragments.map(&:to_s).join(' ')
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveShopifyGraphQL
|
|
4
|
+
module Query
|
|
5
|
+
class Node
|
|
6
|
+
# Renders simple fields and nested fields with children.
|
|
7
|
+
# Examples:
|
|
8
|
+
# - Simple: `id`
|
|
9
|
+
# - With alias: `customerId: id`
|
|
10
|
+
# - Nested: `defaultAddress { city country }`
|
|
11
|
+
class Field < Node
|
|
12
|
+
def to_s
|
|
13
|
+
full_name = "#{field_name_with_alias}#{format_arguments}"
|
|
14
|
+
|
|
15
|
+
return full_name unless has_children?
|
|
16
|
+
|
|
17
|
+
nested_fields = render_children
|
|
18
|
+
"#{full_name} { #{nested_fields.join(' ')} }"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveShopifyGraphQL
|
|
4
|
+
module Query
|
|
5
|
+
class Node
|
|
6
|
+
# Renders GraphQL fragment definitions.
|
|
7
|
+
# Example: `fragment CustomerFragment on Customer { id displayName email }`
|
|
8
|
+
class Fragment < Node
|
|
9
|
+
def to_s
|
|
10
|
+
type_name = @arguments[:on]
|
|
11
|
+
fields = render_children
|
|
12
|
+
"fragment #{@name} on #{type_name} { #{fields.join(' ')} }"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveShopifyGraphQL
|
|
4
|
+
module Query
|
|
5
|
+
class Node
|
|
6
|
+
# Renders a nested connection query (loaded from a parent record).
|
|
7
|
+
# Example:
|
|
8
|
+
# query($id: ID!) {
|
|
9
|
+
# customer(id: $id) {
|
|
10
|
+
# orders(first: 10) {
|
|
11
|
+
# pageInfo {
|
|
12
|
+
# hasNextPage
|
|
13
|
+
# hasPreviousPage
|
|
14
|
+
# startCursor
|
|
15
|
+
# endCursor
|
|
16
|
+
# }
|
|
17
|
+
# nodes {
|
|
18
|
+
# ...OrderFragment
|
|
19
|
+
# }
|
|
20
|
+
# }
|
|
21
|
+
# }
|
|
22
|
+
# }
|
|
23
|
+
class NestedConnection < Node
|
|
24
|
+
attr_reader :query_name, :fragment_name, :variables, :parent_query, :fragments, :singular
|
|
25
|
+
|
|
26
|
+
def initialize(query_name:, fragment_name:, parent_query:, variables: {}, fragments: [], singular: false)
|
|
27
|
+
@query_name = query_name
|
|
28
|
+
@fragment_name = fragment_name
|
|
29
|
+
@variables = variables
|
|
30
|
+
@parent_query = parent_query
|
|
31
|
+
@fragments = fragments
|
|
32
|
+
@singular = singular
|
|
33
|
+
super(name: query_name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_s(*)
|
|
37
|
+
if @singular
|
|
38
|
+
render_singular_connection
|
|
39
|
+
else
|
|
40
|
+
render_nodes_connection
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def render_singular_connection
|
|
47
|
+
"#{fragments_string} query($id: ID!) { #{parent_query} { #{query_name}#{field_signature} { ...#{fragment_name} } } }"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def render_nodes_connection
|
|
51
|
+
"#{fragments_string} query($id: ID!) { #{parent_query} { #{query_name}#{field_signature} { #{PAGE_INFO_FIELDS} nodes { ...#{fragment_name} } } } }"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def fragments_string
|
|
55
|
+
@fragments.map(&:to_s).join(' ')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def field_signature
|
|
59
|
+
params = build_field_parameters(@variables.compact)
|
|
60
|
+
params.empty? ? "" : "(#{params.join(', ')})"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_field_parameters(variables)
|
|
64
|
+
variables.map do |key, value|
|
|
65
|
+
"#{key.to_s.camelize(:lower)}: #{format_inline_value(key, value)}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def format_inline_value(key, value)
|
|
70
|
+
case value
|
|
71
|
+
when Integer, TrueClass, FalseClass then value.to_s
|
|
72
|
+
when String then STRING_KEYS_NEEDING_QUOTES.include?(key.to_sym) ? "\"#{value}\"" : value
|
|
73
|
+
else value.to_s
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveShopifyGraphQL
|
|
4
|
+
module Query
|
|
5
|
+
class Node
|
|
6
|
+
# Renders raw GraphQL strings verbatim.
|
|
7
|
+
# Used for custom GraphQL snippets that don't fit other node types.
|
|
8
|
+
class Raw < Node
|
|
9
|
+
def to_s(*)
|
|
10
|
+
@arguments[:raw_graphql]
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveShopifyGraphQL
|
|
4
|
+
module Query
|
|
5
|
+
class Node
|
|
6
|
+
# Renders a root-level connection query (no parent record).
|
|
7
|
+
# Example:
|
|
8
|
+
# query {
|
|
9
|
+
# customers(first: 10) {
|
|
10
|
+
# pageInfo {
|
|
11
|
+
# hasNextPage
|
|
12
|
+
# hasPreviousPage
|
|
13
|
+
# startCursor
|
|
14
|
+
# endCursor
|
|
15
|
+
# }
|
|
16
|
+
# nodes {
|
|
17
|
+
# ...CustomerFragment
|
|
18
|
+
# }
|
|
19
|
+
# }
|
|
20
|
+
# }
|
|
21
|
+
class RootConnection < Node
|
|
22
|
+
attr_reader :query_name, :fragment_name, :variables, :fragments, :singular
|
|
23
|
+
|
|
24
|
+
def initialize(query_name:, fragment_name:, variables: {}, fragments: [], singular: false)
|
|
25
|
+
@query_name = query_name
|
|
26
|
+
@fragment_name = fragment_name
|
|
27
|
+
@variables = variables
|
|
28
|
+
@fragments = fragments
|
|
29
|
+
@singular = singular
|
|
30
|
+
super(name: query_name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_s(*)
|
|
34
|
+
if @singular
|
|
35
|
+
render_singular_connection
|
|
36
|
+
else
|
|
37
|
+
render_nodes_connection
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def render_singular_connection
|
|
44
|
+
"#{fragments_string} query { #{query_name}#{field_signature} { ...#{fragment_name} } }"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def render_nodes_connection
|
|
48
|
+
"#{fragments_string} query { #{query_name}#{field_signature} { #{PAGE_INFO_FIELDS} nodes { ...#{fragment_name} } } }"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def fragments_string
|
|
52
|
+
@fragments.map(&:to_s).join(' ')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def field_signature
|
|
56
|
+
params = build_field_parameters(@variables.compact)
|
|
57
|
+
params.empty? ? "" : "(#{params.join(', ')})"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_field_parameters(variables)
|
|
61
|
+
variables.map do |key, value|
|
|
62
|
+
"#{key.to_s.camelize(:lower)}: #{format_inline_value(key, value)}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def format_inline_value(key, value)
|
|
67
|
+
case value
|
|
68
|
+
when Integer, TrueClass, FalseClass then value.to_s
|
|
69
|
+
when String then STRING_KEYS_NEEDING_QUOTES.include?(key.to_sym) ? "\"#{value}\"" : value
|
|
70
|
+
else value.to_s
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveShopifyGraphQL
|
|
4
|
+
module Query
|
|
5
|
+
class Node
|
|
6
|
+
# Renders a single record query by ID.
|
|
7
|
+
# Example:
|
|
8
|
+
# query getCustomer($id: ID!) {
|
|
9
|
+
# customer(id: $id) {
|
|
10
|
+
# ...CustomerFragment
|
|
11
|
+
# }
|
|
12
|
+
# }
|
|
13
|
+
class SingleRecord < Node
|
|
14
|
+
attr_reader :model_type, :query_name, :fragment_name, :fragments
|
|
15
|
+
|
|
16
|
+
def initialize(model_type:, query_name:, fragment_name:, fragments: [])
|
|
17
|
+
@model_type = model_type
|
|
18
|
+
@query_name = query_name
|
|
19
|
+
@fragment_name = fragment_name
|
|
20
|
+
@fragments = fragments
|
|
21
|
+
super(name: query_name)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_s
|
|
25
|
+
"#{fragments_string} query get#{model_type}($id: ID!) { #{query_name}(id: $id) { ...#{fragment_name} } }"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def fragments_string
|
|
31
|
+
@fragments.map(&:to_s).join(' ')
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveShopifyGraphQL
|
|
4
|
+
module Query
|
|
5
|
+
class Node
|
|
6
|
+
# Renders singular association fields (has_one relationships).
|
|
7
|
+
# Example: `defaultAddress { city country zip }`
|
|
8
|
+
class Singular < Node
|
|
9
|
+
def to_s
|
|
10
|
+
nested_fields = render_children
|
|
11
|
+
"#{field_name_with_alias}#{format_arguments} { #{nested_fields.join(' ')} }"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveShopifyGraphQL
|
|
4
|
+
module Query
|
|
5
|
+
# Abstract base class for all GraphQL query nodes.
|
|
6
|
+
# Provides shared attributes and helper methods for node rendering.
|
|
7
|
+
#
|
|
8
|
+
# Subclasses:
|
|
9
|
+
# - Node::Field - Simple and nested fields
|
|
10
|
+
# - Node::Singular - Singular associations (has_one)
|
|
11
|
+
# - Node::Connection - Collection associations using nodes pattern
|
|
12
|
+
# - Node::Fragment - Fragment definitions
|
|
13
|
+
# - Node::Raw - Raw GraphQL strings
|
|
14
|
+
# - Node::SingleRecord - Single record queries by ID
|
|
15
|
+
# - Node::CurrentCustomer - ID-less queries (Customer Account API)
|
|
16
|
+
# - Node::Collection - Collection queries with optional pagination
|
|
17
|
+
# - Node::NestedConnection - Nested connection queries
|
|
18
|
+
# - Node::RootConnection - Root-level connection queries
|
|
19
|
+
class Node
|
|
20
|
+
# Shared constants for query formatting
|
|
21
|
+
PAGE_INFO_FIELDS = "pageInfo { hasNextPage hasPreviousPage startCursor endCursor }"
|
|
22
|
+
STRING_KEYS_NEEDING_QUOTES = %i[query after before].freeze
|
|
23
|
+
attr_reader :name, :alias_name, :arguments, :children
|
|
24
|
+
|
|
25
|
+
# @param name [String] The field name (e.g., 'id', 'displayName', 'orders')
|
|
26
|
+
# @param alias_name [String] Optional field alias (e.g., 'myAlias' for 'myAlias: fieldName')
|
|
27
|
+
# @param arguments [Hash] Field arguments (e.g., { first: 10, sortKey: 'CREATED_AT' })
|
|
28
|
+
# @param children [Array<Query::Node>] Child nodes for nested structures
|
|
29
|
+
def initialize(name:, alias_name: nil, arguments: {}, children: [])
|
|
30
|
+
@name = name
|
|
31
|
+
@alias_name = alias_name
|
|
32
|
+
@arguments = arguments
|
|
33
|
+
@children = children
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Add a child node
|
|
37
|
+
def add_child(node)
|
|
38
|
+
@children << node
|
|
39
|
+
node
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Check if node has children
|
|
43
|
+
def has_children?
|
|
44
|
+
@children.any?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Convert node to GraphQL string - must be implemented by subclasses
|
|
48
|
+
def to_s
|
|
49
|
+
raise NotImplementedError, "#{self.class} must implement #to_s"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def field_name_with_alias
|
|
55
|
+
@alias_name ? "#{@alias_name}: #{@name}" : @name
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def format_arguments
|
|
59
|
+
return "" if @arguments.empty?
|
|
60
|
+
|
|
61
|
+
# Filter out internal arguments (like :on for fragments)
|
|
62
|
+
graphql_args = @arguments.except(:on)
|
|
63
|
+
return "" if graphql_args.empty?
|
|
64
|
+
|
|
65
|
+
args = graphql_args.map do |key, value|
|
|
66
|
+
graphql_key = key.to_s.camelize(:lower)
|
|
67
|
+
formatted_value = format_argument_value(key, value)
|
|
68
|
+
"#{graphql_key}: #{formatted_value}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
"(#{args.join(', ')})"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def format_argument_value(key, value)
|
|
75
|
+
case value
|
|
76
|
+
when String
|
|
77
|
+
# Keys that need quoted string values
|
|
78
|
+
if %i[namespace key query].include?(key.to_sym)
|
|
79
|
+
"\"#{value}\""
|
|
80
|
+
else
|
|
81
|
+
value
|
|
82
|
+
end
|
|
83
|
+
when Symbol
|
|
84
|
+
value.to_s
|
|
85
|
+
else
|
|
86
|
+
value
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def render_children
|
|
91
|
+
@children.map(&:to_s)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|