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
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveShopifyGraphQL::Model::GraphqlTypeResolver
|
|
4
|
+
# Centralizes GraphQL type resolution logic.
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
class_methods do
|
|
8
|
+
# Set or get the base GraphQL type for this model.
|
|
9
|
+
#
|
|
10
|
+
# @param type [String, nil] The GraphQL type name to set, or nil to get
|
|
11
|
+
# @return [String] The GraphQL type name
|
|
12
|
+
# @raise [NotImplementedError] If no type is defined
|
|
13
|
+
def graphql_type(type = nil)
|
|
14
|
+
if type
|
|
15
|
+
if @current_loader_context
|
|
16
|
+
@loader_graphql_types ||= {}
|
|
17
|
+
@loader_graphql_types[@current_loader_context] = type
|
|
18
|
+
else
|
|
19
|
+
@base_graphql_type = type
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
@base_graphql_type || raise(NotImplementedError, "#{self} must define graphql_type")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Get the GraphQL type for a specific loader class.
|
|
27
|
+
# Resolution order:
|
|
28
|
+
# 1. Loader-specific type defined via `for_loader`
|
|
29
|
+
# 2. Base graphql_type defined on the model
|
|
30
|
+
# 3. Type defined on the loader class itself
|
|
31
|
+
# 4. Inferred from model class name
|
|
32
|
+
#
|
|
33
|
+
# @param loader_class [Class] The loader class to resolve type for
|
|
34
|
+
# @return [String] The resolved GraphQL type
|
|
35
|
+
# @raise [NotImplementedError] If no type can be resolved
|
|
36
|
+
def graphql_type_for_loader(loader_class)
|
|
37
|
+
# 1. Check loader-specific override
|
|
38
|
+
return @loader_graphql_types[loader_class] if @loader_graphql_types&.key?(loader_class)
|
|
39
|
+
|
|
40
|
+
# 2. Check base graphql_type
|
|
41
|
+
return @base_graphql_type if @base_graphql_type
|
|
42
|
+
|
|
43
|
+
# 3. Check loader class itself
|
|
44
|
+
loader_type = loader_class.instance_variable_get(:@graphql_type)
|
|
45
|
+
return loader_type if loader_type
|
|
46
|
+
|
|
47
|
+
# 4. Infer from model name
|
|
48
|
+
return name.demodulize if respond_to?(:name) && name
|
|
49
|
+
|
|
50
|
+
raise NotImplementedError,
|
|
51
|
+
"#{self} must define graphql_type or #{loader_class} must define graphql_type"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Resolve the GraphQL type from any source (model class, loader, or value).
|
|
55
|
+
# Useful for external callers that need to resolve type from various inputs.
|
|
56
|
+
#
|
|
57
|
+
# @param model_class [Class, nil] The model class
|
|
58
|
+
# @param loader_class [Class, nil] The loader class
|
|
59
|
+
# @return [String] The resolved GraphQL type
|
|
60
|
+
def resolve_graphql_type(model_class: nil, loader_class: nil)
|
|
61
|
+
if model_class.respond_to?(:graphql_type_for_loader) && loader_class
|
|
62
|
+
model_class.graphql_type_for_loader(loader_class)
|
|
63
|
+
elsif model_class.respond_to?(:graphql_type)
|
|
64
|
+
model_class.graphql_type
|
|
65
|
+
elsif loader_class.respond_to?(:graphql_type)
|
|
66
|
+
loader_class.graphql_type
|
|
67
|
+
elsif model_class.respond_to?(:name) && model_class.name
|
|
68
|
+
model_class.name.demodulize
|
|
69
|
+
else
|
|
70
|
+
raise ArgumentError, "Cannot resolve graphql_type from provided arguments"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Module-level resolver for convenience
|
|
76
|
+
def self.resolve(model_class: nil, loader_class: nil)
|
|
77
|
+
if model_class.respond_to?(:graphql_type_for_loader) && loader_class
|
|
78
|
+
model_class.graphql_type_for_loader(loader_class)
|
|
79
|
+
elsif model_class.respond_to?(:graphql_type)
|
|
80
|
+
model_class.graphql_type
|
|
81
|
+
elsif loader_class.respond_to?(:graphql_type)
|
|
82
|
+
loader_class.graphql_type
|
|
83
|
+
elsif model_class.respond_to?(:name) && model_class.name
|
|
84
|
+
model_class.name.demodulize
|
|
85
|
+
else
|
|
86
|
+
raise ArgumentError, "Cannot resolve graphql_type"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveShopifyGraphQL::Model::LoaderSwitchable
|
|
4
|
+
# Provides capability to switch between different loaders within the same model
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
# Generic method to execute with a specific loader
|
|
8
|
+
# @param loader_class [Class] The loader class to use
|
|
9
|
+
# @yield [Object] Block to execute with the loader
|
|
10
|
+
# @return [Object] Result of the block
|
|
11
|
+
def with_loader(loader_class, &_block)
|
|
12
|
+
old_loader = Thread.current[:active_shopify_graphql_loader]
|
|
13
|
+
Thread.current[:active_shopify_graphql_loader] = loader_class.new(self.class)
|
|
14
|
+
|
|
15
|
+
if block_given?
|
|
16
|
+
yield(self)
|
|
17
|
+
else
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
ensure
|
|
21
|
+
Thread.current[:active_shopify_graphql_loader] = old_loader
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Executes with the admin API loader
|
|
25
|
+
# @return [self]
|
|
26
|
+
def with_admin_api(&block)
|
|
27
|
+
with_loader(ActiveShopifyGraphQL::Loaders::AdminApiLoader, &block)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Executes with the customer account API loader
|
|
31
|
+
# @return [self]
|
|
32
|
+
def with_customer_account_api(&block)
|
|
33
|
+
with_loader(ActiveShopifyGraphQL::Loaders::CustomerAccountApiLoader, &block)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class_methods do
|
|
37
|
+
# @!method use_loader(loader_class)
|
|
38
|
+
# Sets the default loader class for this model.
|
|
39
|
+
#
|
|
40
|
+
# @param loader_class [Class] The loader class to use as default
|
|
41
|
+
# @example
|
|
42
|
+
# class Customer < ActiveRecord::Base
|
|
43
|
+
# use_loader ActiveShopifyGraphQL::Loaders::CustomerAccountApiLoader
|
|
44
|
+
# end
|
|
45
|
+
def use_loader(loader_class)
|
|
46
|
+
@default_loader_class = loader_class
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Define loader-specific attribute and graphql_type overrides
|
|
50
|
+
# @param loader_class [Class] The loader class to override attributes for
|
|
51
|
+
def for_loader(loader_class, &block)
|
|
52
|
+
@current_loader_context = loader_class
|
|
53
|
+
@loader_contexts ||= {}
|
|
54
|
+
@loader_contexts[loader_class] ||= {}
|
|
55
|
+
instance_eval(&block) if block_given?
|
|
56
|
+
@current_loader_context = nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Class-level method to execute with admin API loader
|
|
60
|
+
# @return [LoaderProxy] Proxy object with find method
|
|
61
|
+
def with_admin_api
|
|
62
|
+
ActiveShopifyGraphQL::LoaderProxy.new(self, ActiveShopifyGraphQL::Loaders::AdminApiLoader.new(self))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Class-level method to execute with customer account API loader
|
|
66
|
+
# @return [LoaderProxy] Proxy object with find method
|
|
67
|
+
def with_customer_account_api(token = nil)
|
|
68
|
+
ActiveShopifyGraphQL::LoaderProxy.new(self, ActiveShopifyGraphQL::Loaders::CustomerAccountApiLoader.new(self, token))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# Returns the default loader class (either set via DSL or inferred)
|
|
74
|
+
# @return [Class] The default loader class
|
|
75
|
+
def default_loader_class
|
|
76
|
+
@default_loader_class ||= ActiveShopifyGraphQL::Loaders::AdminApiLoader
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveShopifyGraphQL::Model::MetafieldAttributes
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
class_methods do
|
|
7
|
+
# Define a metafield attribute for this model.
|
|
8
|
+
#
|
|
9
|
+
# @param name [Symbol] The Ruby attribute name
|
|
10
|
+
# @param namespace [String] The metafield namespace
|
|
11
|
+
# @param key [String] The metafield key
|
|
12
|
+
# @param type [Symbol] The type for coercion (:string, :integer, :float, :boolean, :datetime, :json)
|
|
13
|
+
# @param null [Boolean] Whether the attribute can be null (default: true)
|
|
14
|
+
# @param default [Object] Default value when GraphQL response is nil
|
|
15
|
+
# @param transform [Proc] Custom transform block for the value
|
|
16
|
+
def metafield_attribute(name, namespace:, key:, type: :string, null: true, default: nil, transform: nil)
|
|
17
|
+
@metafields ||= {}
|
|
18
|
+
@metafields[name] = { namespace: namespace, key: key, type: type }
|
|
19
|
+
|
|
20
|
+
# Build metafield config
|
|
21
|
+
alias_name = "#{infer_path(name)}Metafield"
|
|
22
|
+
value_field = type == :json ? 'jsonValue' : 'value'
|
|
23
|
+
path = "#{alias_name}.#{value_field}"
|
|
24
|
+
|
|
25
|
+
config = {
|
|
26
|
+
path: path,
|
|
27
|
+
type: type,
|
|
28
|
+
null: null,
|
|
29
|
+
default: default,
|
|
30
|
+
transform: transform,
|
|
31
|
+
is_metafield: true,
|
|
32
|
+
metafield_alias: alias_name,
|
|
33
|
+
metafield_namespace: namespace,
|
|
34
|
+
metafield_key: key
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if @current_loader_context
|
|
38
|
+
@loader_contexts[@current_loader_context][name] = config
|
|
39
|
+
else
|
|
40
|
+
@base_attributes ||= {}
|
|
41
|
+
@base_attributes[name] = config
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
attr_accessor name unless method_defined?(name) || method_defined?("#{name}=")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Get metafields defined for this model
|
|
48
|
+
def metafields
|
|
49
|
+
@metafields || {}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Infer GraphQL path from Ruby attribute name (delegates to Attributes if available)
|
|
55
|
+
def infer_path(name)
|
|
56
|
+
name.to_s.gsub(/_([a-z])/) { ::Regexp.last_match(1).upcase }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -1,25 +1,39 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ActiveShopifyGraphQL
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
# Base class for all GraphQL-backed models.
|
|
5
|
+
#
|
|
6
|
+
# Models should inherit from this class (typically via an ApplicationShopifyGqlRecord
|
|
7
|
+
# intermediate class) to gain ActiveRecord-like functionality for Shopify GraphQL APIs.
|
|
8
|
+
#
|
|
9
|
+
# @example Creating an ApplicationShopifyGqlRecord base class
|
|
10
|
+
# class ApplicationShopifyGqlRecord < ActiveShopifyGraphQL::Model
|
|
11
|
+
# attribute :id, transform: ->(id) { id.split("/").last }
|
|
12
|
+
# attribute :gid, path: "id"
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# @example Defining a model
|
|
16
|
+
# class Customer < ApplicationShopifyGqlRecord
|
|
17
|
+
# graphql_type "Customer"
|
|
18
|
+
#
|
|
19
|
+
# attribute :first_name
|
|
20
|
+
# attribute :email, path: "defaultEmailAddress.emailAddress"
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
class Model
|
|
24
|
+
include ActiveModel::AttributeAssignment
|
|
25
|
+
include ActiveModel::Validations
|
|
26
|
+
extend ActiveModel::Naming
|
|
6
27
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
include ActiveShopifyGraphQL::Connections
|
|
15
|
-
include ActiveShopifyGraphQL::Attributes
|
|
16
|
-
include ActiveShopifyGraphQL::MetafieldAttributes
|
|
17
|
-
include ActiveShopifyGraphQL::LoaderSwitchable
|
|
18
|
-
end
|
|
28
|
+
include GraphqlTypeResolver
|
|
29
|
+
include FinderMethods
|
|
30
|
+
include Associations
|
|
31
|
+
include Connections
|
|
32
|
+
include Attributes
|
|
33
|
+
include MetafieldAttributes
|
|
34
|
+
include LoaderSwitchable
|
|
19
35
|
|
|
20
36
|
def initialize(attributes = {})
|
|
21
|
-
super()
|
|
22
|
-
|
|
23
37
|
# Extract connection cache if present and populate inverse caches
|
|
24
38
|
if attributes.key?(:_connection_cache)
|
|
25
39
|
@_connection_cache = attributes.delete(:_connection_cache)
|
|
@@ -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
|