active_shopify_graphql 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -0
- data/README.md +187 -56
- data/lib/active_shopify_graphql/configuration.rb +2 -15
- data/lib/active_shopify_graphql/connections/connection_loader.rb +141 -0
- data/lib/active_shopify_graphql/gid_helper.rb +2 -0
- data/lib/active_shopify_graphql/graphql_associations.rb +245 -0
- data/lib/active_shopify_graphql/loader.rb +147 -126
- data/lib/active_shopify_graphql/loader_context.rb +2 -47
- data/lib/active_shopify_graphql/loader_proxy.rb +67 -0
- data/lib/active_shopify_graphql/loaders/admin_api_loader.rb +1 -17
- data/lib/active_shopify_graphql/loaders/customer_account_api_loader.rb +10 -26
- data/lib/active_shopify_graphql/model/associations.rb +94 -0
- data/lib/active_shopify_graphql/model/attributes.rb +48 -0
- data/lib/active_shopify_graphql/model/connections.rb +174 -0
- data/lib/active_shopify_graphql/model/finder_methods.rb +155 -0
- data/lib/active_shopify_graphql/model/graphql_type_resolver.rb +89 -0
- data/lib/active_shopify_graphql/model/loader_switchable.rb +79 -0
- data/lib/active_shopify_graphql/model/metafield_attributes.rb +59 -0
- data/lib/active_shopify_graphql/{base.rb → model.rb} +30 -16
- data/lib/active_shopify_graphql/model_builder.rb +53 -0
- data/lib/active_shopify_graphql/query/node/collection.rb +78 -0
- data/lib/active_shopify_graphql/query/node/connection.rb +16 -0
- data/lib/active_shopify_graphql/query/node/current_customer.rb +37 -0
- data/lib/active_shopify_graphql/query/node/field.rb +23 -0
- data/lib/active_shopify_graphql/query/node/fragment.rb +17 -0
- data/lib/active_shopify_graphql/query/node/nested_connection.rb +79 -0
- data/lib/active_shopify_graphql/query/node/raw.rb +15 -0
- data/lib/active_shopify_graphql/query/node/root_connection.rb +76 -0
- data/lib/active_shopify_graphql/query/node/single_record.rb +36 -0
- data/lib/active_shopify_graphql/query/node/singular.rb +16 -0
- data/lib/active_shopify_graphql/query/node.rb +95 -0
- data/lib/active_shopify_graphql/query/query_builder.rb +290 -0
- data/lib/active_shopify_graphql/query/relation.rb +424 -0
- data/lib/active_shopify_graphql/query/scope.rb +219 -0
- data/lib/active_shopify_graphql/response/page_info.rb +40 -0
- data/lib/active_shopify_graphql/response/paginated_result.rb +114 -0
- data/lib/active_shopify_graphql/response/response_mapper.rb +242 -0
- data/lib/active_shopify_graphql/search_query/hash_condition_formatter.rb +107 -0
- data/lib/active_shopify_graphql/search_query/parameter_binder.rb +68 -0
- data/lib/active_shopify_graphql/search_query/value_sanitizer.rb +24 -0
- data/lib/active_shopify_graphql/search_query.rb +34 -84
- data/lib/active_shopify_graphql/version.rb +1 -1
- data/lib/active_shopify_graphql.rb +30 -28
- metadata +47 -15
- data/lib/active_shopify_graphql/associations.rb +0 -90
- data/lib/active_shopify_graphql/attributes.rb +0 -50
- data/lib/active_shopify_graphql/connection_loader.rb +0 -96
- data/lib/active_shopify_graphql/connections.rb +0 -198
- data/lib/active_shopify_graphql/finder_methods.rb +0 -159
- data/lib/active_shopify_graphql/fragment_builder.rb +0 -228
- data/lib/active_shopify_graphql/graphql_type_resolver.rb +0 -91
- data/lib/active_shopify_graphql/includes_scope.rb +0 -48
- data/lib/active_shopify_graphql/loader_switchable.rb +0 -180
- data/lib/active_shopify_graphql/metafield_attributes.rb +0 -61
- data/lib/active_shopify_graphql/query_node.rb +0 -173
- data/lib/active_shopify_graphql/query_tree.rb +0 -225
- data/lib/active_shopify_graphql/response_mapper.rb +0 -249
|
@@ -1,35 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
7
|
-
require
|
|
8
|
-
require
|
|
3
|
+
require "active_support"
|
|
4
|
+
require "active_support/concern"
|
|
5
|
+
require "active_model"
|
|
6
|
+
require "active_model/attribute_assignment"
|
|
7
|
+
require "active_model/validations"
|
|
8
|
+
require "active_model/naming"
|
|
9
|
+
require "globalid"
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
require_relative "active_shopify_graphql/query_tree"
|
|
18
|
-
require_relative "active_shopify_graphql/response_mapper"
|
|
19
|
-
require_relative "active_shopify_graphql/connection_loader"
|
|
20
|
-
require_relative "active_shopify_graphql/loader"
|
|
21
|
-
require_relative "active_shopify_graphql/loaders/admin_api_loader"
|
|
22
|
-
require_relative "active_shopify_graphql/loaders/customer_account_api_loader"
|
|
23
|
-
require_relative "active_shopify_graphql/loader_switchable"
|
|
24
|
-
require_relative "active_shopify_graphql/finder_methods"
|
|
25
|
-
require_relative "active_shopify_graphql/associations"
|
|
26
|
-
require_relative "active_shopify_graphql/includes_scope"
|
|
27
|
-
require_relative "active_shopify_graphql/connections"
|
|
28
|
-
require_relative "active_shopify_graphql/attributes"
|
|
29
|
-
require_relative "active_shopify_graphql/metafield_attributes"
|
|
30
|
-
require_relative "active_shopify_graphql/search_query"
|
|
31
|
-
require_relative "active_shopify_graphql/base"
|
|
11
|
+
require "zeitwerk"
|
|
12
|
+
loader = Zeitwerk::Loader.for_gem
|
|
13
|
+
loader.inflector.inflect(
|
|
14
|
+
"active_shopify_graphql" => "ActiveShopifyGraphQL",
|
|
15
|
+
"graphql_associations" => "GraphQLAssociations"
|
|
16
|
+
)
|
|
17
|
+
loader.setup
|
|
32
18
|
|
|
33
19
|
module ActiveShopifyGraphQL
|
|
34
20
|
class Error < StandardError; end
|
|
21
|
+
class ObjectNotFoundError < Error; end
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
def configuration
|
|
25
|
+
@configuration ||= Configuration.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def configure
|
|
29
|
+
yield(configuration)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Reset configuration (useful for testing)
|
|
33
|
+
def reset_configuration!
|
|
34
|
+
@configuration = Configuration.new
|
|
35
|
+
end
|
|
36
|
+
end
|
|
35
37
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_shopify_graphql
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nicolò Rebughini
|
|
@@ -52,6 +52,20 @@ dependencies:
|
|
|
52
52
|
- - "~>"
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '1.3'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: zeitwerk
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '2.6'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '2.6'
|
|
55
69
|
description: ActiveShopifyGraphQL provides an ActiveRecord-like interface for interacting
|
|
56
70
|
with Shopify's GraphQL APIs, supporting both Admin API and Customer Account API
|
|
57
71
|
with automatic query building and response mapping.
|
|
@@ -69,28 +83,46 @@ files:
|
|
|
69
83
|
- README.md
|
|
70
84
|
- Rakefile
|
|
71
85
|
- lib/active_shopify_graphql.rb
|
|
72
|
-
- lib/active_shopify_graphql/associations.rb
|
|
73
|
-
- lib/active_shopify_graphql/attributes.rb
|
|
74
|
-
- lib/active_shopify_graphql/base.rb
|
|
75
86
|
- lib/active_shopify_graphql/configuration.rb
|
|
76
|
-
- lib/active_shopify_graphql/connection_loader.rb
|
|
77
|
-
- lib/active_shopify_graphql/connections.rb
|
|
87
|
+
- lib/active_shopify_graphql/connections/connection_loader.rb
|
|
78
88
|
- lib/active_shopify_graphql/connections/connection_proxy.rb
|
|
79
|
-
- lib/active_shopify_graphql/finder_methods.rb
|
|
80
|
-
- lib/active_shopify_graphql/fragment_builder.rb
|
|
81
89
|
- lib/active_shopify_graphql/gid_helper.rb
|
|
82
|
-
- lib/active_shopify_graphql/
|
|
83
|
-
- lib/active_shopify_graphql/includes_scope.rb
|
|
90
|
+
- lib/active_shopify_graphql/graphql_associations.rb
|
|
84
91
|
- lib/active_shopify_graphql/loader.rb
|
|
85
92
|
- lib/active_shopify_graphql/loader_context.rb
|
|
86
|
-
- lib/active_shopify_graphql/
|
|
93
|
+
- lib/active_shopify_graphql/loader_proxy.rb
|
|
87
94
|
- lib/active_shopify_graphql/loaders/admin_api_loader.rb
|
|
88
95
|
- lib/active_shopify_graphql/loaders/customer_account_api_loader.rb
|
|
89
|
-
- lib/active_shopify_graphql/
|
|
90
|
-
- lib/active_shopify_graphql/
|
|
91
|
-
- lib/active_shopify_graphql/
|
|
92
|
-
- lib/active_shopify_graphql/
|
|
96
|
+
- lib/active_shopify_graphql/model.rb
|
|
97
|
+
- lib/active_shopify_graphql/model/associations.rb
|
|
98
|
+
- lib/active_shopify_graphql/model/attributes.rb
|
|
99
|
+
- lib/active_shopify_graphql/model/connections.rb
|
|
100
|
+
- lib/active_shopify_graphql/model/finder_methods.rb
|
|
101
|
+
- lib/active_shopify_graphql/model/graphql_type_resolver.rb
|
|
102
|
+
- lib/active_shopify_graphql/model/loader_switchable.rb
|
|
103
|
+
- lib/active_shopify_graphql/model/metafield_attributes.rb
|
|
104
|
+
- lib/active_shopify_graphql/model_builder.rb
|
|
105
|
+
- lib/active_shopify_graphql/query/node.rb
|
|
106
|
+
- lib/active_shopify_graphql/query/node/collection.rb
|
|
107
|
+
- lib/active_shopify_graphql/query/node/connection.rb
|
|
108
|
+
- lib/active_shopify_graphql/query/node/current_customer.rb
|
|
109
|
+
- lib/active_shopify_graphql/query/node/field.rb
|
|
110
|
+
- lib/active_shopify_graphql/query/node/fragment.rb
|
|
111
|
+
- lib/active_shopify_graphql/query/node/nested_connection.rb
|
|
112
|
+
- lib/active_shopify_graphql/query/node/raw.rb
|
|
113
|
+
- lib/active_shopify_graphql/query/node/root_connection.rb
|
|
114
|
+
- lib/active_shopify_graphql/query/node/single_record.rb
|
|
115
|
+
- lib/active_shopify_graphql/query/node/singular.rb
|
|
116
|
+
- lib/active_shopify_graphql/query/query_builder.rb
|
|
117
|
+
- lib/active_shopify_graphql/query/relation.rb
|
|
118
|
+
- lib/active_shopify_graphql/query/scope.rb
|
|
119
|
+
- lib/active_shopify_graphql/response/page_info.rb
|
|
120
|
+
- lib/active_shopify_graphql/response/paginated_result.rb
|
|
121
|
+
- lib/active_shopify_graphql/response/response_mapper.rb
|
|
93
122
|
- lib/active_shopify_graphql/search_query.rb
|
|
123
|
+
- lib/active_shopify_graphql/search_query/hash_condition_formatter.rb
|
|
124
|
+
- lib/active_shopify_graphql/search_query/parameter_binder.rb
|
|
125
|
+
- lib/active_shopify_graphql/search_query/value_sanitizer.rb
|
|
94
126
|
- lib/active_shopify_graphql/version.rb
|
|
95
127
|
homepage: https://github.com/nebulab/active_shopify_graphql
|
|
96
128
|
licenses:
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveShopifyGraphQL
|
|
4
|
-
# Handles associations between ActiveShopifyGraphQL objects and ActiveRecord objects
|
|
5
|
-
module Associations
|
|
6
|
-
extend ActiveSupport::Concern
|
|
7
|
-
|
|
8
|
-
included do
|
|
9
|
-
class << self
|
|
10
|
-
attr_accessor :associations
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
self.associations = {}
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
class_methods do
|
|
17
|
-
def has_many(name, class_name: nil, foreign_key: nil, primary_key: nil)
|
|
18
|
-
association_class_name = class_name || name.to_s.classify
|
|
19
|
-
association_foreign_key = foreign_key || "shopify_#{model_name.element.downcase}_id"
|
|
20
|
-
association_primary_key = primary_key || :id
|
|
21
|
-
|
|
22
|
-
# Store association metadata
|
|
23
|
-
associations[name] = {
|
|
24
|
-
type: :has_many,
|
|
25
|
-
class_name: association_class_name,
|
|
26
|
-
foreign_key: association_foreign_key,
|
|
27
|
-
primary_key: association_primary_key
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
# Define the association method
|
|
31
|
-
define_method name do
|
|
32
|
-
return @_association_cache[name] if @_association_cache&.key?(name)
|
|
33
|
-
|
|
34
|
-
@_association_cache ||= {}
|
|
35
|
-
|
|
36
|
-
primary_key_value = send(association_primary_key)
|
|
37
|
-
return @_association_cache[name] = [] if primary_key_value.blank?
|
|
38
|
-
|
|
39
|
-
# Extract numeric ID from Shopify GID if needed
|
|
40
|
-
primary_key_value = primary_key_value.to_plain_id if primary_key_value.gid?
|
|
41
|
-
|
|
42
|
-
association_class = association_class_name.constantize
|
|
43
|
-
@_association_cache[name] = association_class.where(association_foreign_key => primary_key_value)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Define the association setter method for testing/mocking
|
|
47
|
-
define_method "#{name}=" do |value|
|
|
48
|
-
@_association_cache ||= {}
|
|
49
|
-
@_association_cache[name] = value
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def has_one(name, class_name: nil, foreign_key: nil, primary_key: nil)
|
|
54
|
-
association_class_name = class_name || name.to_s.classify
|
|
55
|
-
association_foreign_key = foreign_key || "shopify_#{model_name.element.downcase}_id"
|
|
56
|
-
association_primary_key = primary_key || :id
|
|
57
|
-
|
|
58
|
-
# Store association metadata
|
|
59
|
-
associations[name] = {
|
|
60
|
-
type: :has_one,
|
|
61
|
-
class_name: association_class_name,
|
|
62
|
-
foreign_key: association_foreign_key,
|
|
63
|
-
primary_key: association_primary_key
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
# Define the association method
|
|
67
|
-
define_method name do
|
|
68
|
-
return @_association_cache[name] if @_association_cache&.key?(name)
|
|
69
|
-
|
|
70
|
-
@_association_cache ||= {}
|
|
71
|
-
|
|
72
|
-
primary_key_value = send(association_primary_key)
|
|
73
|
-
return @_association_cache[name] = nil if primary_key_value.blank?
|
|
74
|
-
|
|
75
|
-
# Extract numeric ID from Shopify GID if needed
|
|
76
|
-
primary_key_value = primary_key_value.to_plain_id if primary_key_value.gid?
|
|
77
|
-
|
|
78
|
-
association_class = association_class_name.constantize
|
|
79
|
-
@_association_cache[name] = association_class.find_by(association_foreign_key => primary_key_value)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Define the association setter method for testing/mocking
|
|
83
|
-
define_method "#{name}=" do |value|
|
|
84
|
-
@_association_cache ||= {}
|
|
85
|
-
@_association_cache[name] = value
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveShopifyGraphQL
|
|
4
|
-
module Attributes
|
|
5
|
-
extend ActiveSupport::Concern
|
|
6
|
-
|
|
7
|
-
class_methods do
|
|
8
|
-
# Define an attribute with automatic GraphQL path inference and type coercion.
|
|
9
|
-
#
|
|
10
|
-
# @param name [Symbol] The Ruby attribute name
|
|
11
|
-
# @param path [String] The GraphQL field path (auto-inferred if not provided)
|
|
12
|
-
# @param type [Symbol] The type for coercion (:string, :integer, :float, :boolean, :datetime)
|
|
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
|
-
# @param raw_graphql [String] Raw GraphQL string to inject directly (escape hatch for unsupported features)
|
|
17
|
-
def attribute(name, path: nil, type: :string, null: true, default: nil, transform: nil, raw_graphql: nil)
|
|
18
|
-
path ||= infer_path(name)
|
|
19
|
-
config = { path: path, type: type, null: null, default: default, transform: transform, raw_graphql: raw_graphql }
|
|
20
|
-
|
|
21
|
-
if @current_loader_context
|
|
22
|
-
# Store in loader-specific context
|
|
23
|
-
@loader_contexts[@current_loader_context][name] = config
|
|
24
|
-
else
|
|
25
|
-
# Store in base attributes
|
|
26
|
-
@base_attributes ||= {}
|
|
27
|
-
@base_attributes[name] = config
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Always create attr_accessor
|
|
31
|
-
attr_accessor name unless method_defined?(name) || method_defined?("#{name}=")
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Get attributes for a specific loader class, merging base with loader-specific overrides.
|
|
35
|
-
def attributes_for_loader(loader_class)
|
|
36
|
-
base = @base_attributes || {}
|
|
37
|
-
overrides = @loader_contexts&.dig(loader_class) || {}
|
|
38
|
-
|
|
39
|
-
base.merge(overrides) { |_key, base_val, override_val| base_val.merge(override_val) }
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
private
|
|
43
|
-
|
|
44
|
-
# Infer GraphQL path from Ruby attribute name (snake_case -> camelCase)
|
|
45
|
-
def infer_path(name)
|
|
46
|
-
name.to_s.gsub(/_([a-z])/) { ::Regexp.last_match(1).upcase }
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'global_id'
|
|
4
|
-
|
|
5
|
-
module ActiveShopifyGraphQL
|
|
6
|
-
# Handles loading records for GraphQL connections.
|
|
7
|
-
# Refactored to use LoaderContext for cleaner parameter passing.
|
|
8
|
-
class ConnectionLoader
|
|
9
|
-
attr_reader :context
|
|
10
|
-
|
|
11
|
-
def initialize(context, loader_instance:)
|
|
12
|
-
@context = context
|
|
13
|
-
@loader_instance = loader_instance
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# Load records for a connection query
|
|
17
|
-
# @param query_name [String] The connection field name (e.g., 'orders', 'addresses')
|
|
18
|
-
# @param variables [Hash] The GraphQL variables (first, sort_key, reverse, query)
|
|
19
|
-
# @param parent [Object] The parent object that owns this connection
|
|
20
|
-
# @param connection_config [Hash] The connection configuration
|
|
21
|
-
# @return [Array<Object>] Array of model instances
|
|
22
|
-
def load_records(query_name, variables, parent = nil, connection_config = nil)
|
|
23
|
-
is_nested = connection_config&.dig(:nested) || parent.respond_to?(:id)
|
|
24
|
-
|
|
25
|
-
if is_nested && parent
|
|
26
|
-
load_nested_connection(query_name, variables, parent, connection_config)
|
|
27
|
-
else
|
|
28
|
-
load_root_connection(query_name, variables, connection_config)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
|
|
34
|
-
def load_nested_connection(query_name, variables, parent, connection_config)
|
|
35
|
-
parent_type = parent.class.graphql_type_for_loader(@context.loader_class)
|
|
36
|
-
parent_query_name = parent_type.camelize(:lower)
|
|
37
|
-
connection_type = connection_config&.dig(:type) || :connection
|
|
38
|
-
|
|
39
|
-
query = QueryTree.build_connection_query(
|
|
40
|
-
@context,
|
|
41
|
-
query_name: query_name,
|
|
42
|
-
variables: variables,
|
|
43
|
-
parent_query: "#{parent_query_name}(id: $id)",
|
|
44
|
-
connection_type: connection_type
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
parent_id = extract_gid(parent)
|
|
48
|
-
response_data = @loader_instance.perform_graphql_query(query, id: parent_id)
|
|
49
|
-
|
|
50
|
-
return [] if response_data.nil?
|
|
51
|
-
|
|
52
|
-
mapper = ResponseMapper.new(@context)
|
|
53
|
-
mapper.map_nested_connection_response(response_data, query_name, parent, connection_config)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def load_root_connection(query_name, variables, connection_config)
|
|
57
|
-
connection_type = connection_config&.dig(:type) || :connection
|
|
58
|
-
|
|
59
|
-
query = QueryTree.build_connection_query(
|
|
60
|
-
@context,
|
|
61
|
-
query_name: query_name,
|
|
62
|
-
variables: variables,
|
|
63
|
-
parent_query: nil,
|
|
64
|
-
connection_type: connection_type
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
response_data = @loader_instance.perform_graphql_query(query)
|
|
68
|
-
|
|
69
|
-
return [] if response_data.nil?
|
|
70
|
-
|
|
71
|
-
mapper = ResponseMapper.new(@context)
|
|
72
|
-
mapper.map_connection_response(response_data, query_name, connection_config)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def extract_gid(parent)
|
|
76
|
-
return parent.gid if parent.respond_to?(:gid) && !parent.gid.nil?
|
|
77
|
-
|
|
78
|
-
id_value = parent.id
|
|
79
|
-
parent_type = resolve_parent_type(parent)
|
|
80
|
-
|
|
81
|
-
GidHelper.normalize_gid(id_value, parent_type)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def resolve_parent_type(parent)
|
|
85
|
-
klass = parent.class
|
|
86
|
-
|
|
87
|
-
if klass.respond_to?(:graphql_type_for_loader)
|
|
88
|
-
klass.graphql_type_for_loader(@context.loader_class)
|
|
89
|
-
elsif klass.respond_to?(:graphql_type)
|
|
90
|
-
klass.graphql_type
|
|
91
|
-
else
|
|
92
|
-
klass.name
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "connections/connection_proxy"
|
|
4
|
-
|
|
5
|
-
module ActiveShopifyGraphQL
|
|
6
|
-
module Connections
|
|
7
|
-
extend ActiveSupport::Concern
|
|
8
|
-
|
|
9
|
-
included do
|
|
10
|
-
class << self
|
|
11
|
-
attr_accessor :connections
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
self.connections = {}
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
class_methods do
|
|
18
|
-
# Define a singular connection (returns a single object)
|
|
19
|
-
# @see #connection
|
|
20
|
-
def has_one_connected(name, inverse_of: nil, **options)
|
|
21
|
-
connection(name, type: :singular, inverse_of: inverse_of, **options)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Define a plural connection (returns a collection via edges)
|
|
25
|
-
# @see #connection
|
|
26
|
-
def has_many_connected(name, inverse_of: nil, **options)
|
|
27
|
-
connection(name, type: :connection, inverse_of: inverse_of, **options)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Define a GraphQL connection to another ActiveShopifyGraphQL model
|
|
31
|
-
# @param name [Symbol] The connection name (e.g., :orders)
|
|
32
|
-
# @param class_name [String] The target model class name (defaults to name.to_s.classify)
|
|
33
|
-
# @param query_name [String] The GraphQL query field name (auto-determined based on nested/root-level)
|
|
34
|
-
# @param foreign_key [String] The field to filter by (auto-determined for root-level queries)
|
|
35
|
-
# @param loader_class [Class] Custom loader class to use (defaults to model's default loader)
|
|
36
|
-
# @param eager_load [Boolean] Whether to automatically eager load this connection (default: false)
|
|
37
|
-
# @param type [Symbol] The type of connection (:connection, :singular). Default is :connection.
|
|
38
|
-
# @param default_arguments [Hash] Default arguments to pass to the GraphQL query (e.g. first: 10)
|
|
39
|
-
# @param inverse_of [Symbol] The name of the inverse connection on the target model (optional)
|
|
40
|
-
def connection(name, class_name: nil, query_name: nil, foreign_key: nil, loader_class: nil, eager_load: false, type: :connection, default_arguments: {}, inverse_of: nil)
|
|
41
|
-
# Infer defaults
|
|
42
|
-
connection_class_name = class_name || name.to_s.classify
|
|
43
|
-
|
|
44
|
-
# Set query_name - default to camelCase for nested fields
|
|
45
|
-
connection_query_name = query_name || name.to_s.camelize(:lower)
|
|
46
|
-
|
|
47
|
-
connection_loader_class = loader_class
|
|
48
|
-
|
|
49
|
-
# Store connection metadata
|
|
50
|
-
connections[name] = {
|
|
51
|
-
class_name: connection_class_name,
|
|
52
|
-
query_name: connection_query_name,
|
|
53
|
-
foreign_key: foreign_key,
|
|
54
|
-
loader_class: connection_loader_class,
|
|
55
|
-
eager_load: eager_load,
|
|
56
|
-
type: type,
|
|
57
|
-
nested: true, # Always treated as nested (accessed via parent field)
|
|
58
|
-
target_class_name: connection_class_name,
|
|
59
|
-
original_name: name,
|
|
60
|
-
default_arguments: default_arguments,
|
|
61
|
-
inverse_of: inverse_of
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
# Validate inverse relationship if specified (validation is deferred to runtime)
|
|
65
|
-
validate_inverse_of!(name, connection_class_name, inverse_of) if inverse_of
|
|
66
|
-
|
|
67
|
-
# Define the connection method that returns a proxy
|
|
68
|
-
define_method name do |**options|
|
|
69
|
-
# Check if this connection was eager loaded
|
|
70
|
-
return @_connection_cache[name] if @_connection_cache&.key?(name)
|
|
71
|
-
|
|
72
|
-
config = self.class.connections[name]
|
|
73
|
-
if config[:type] == :singular
|
|
74
|
-
# Lazy load singular association
|
|
75
|
-
loader_class = config[:loader_class] || self.class.default_loader.class
|
|
76
|
-
target_class = config[:class_name].constantize
|
|
77
|
-
loader = loader_class.new(target_class)
|
|
78
|
-
|
|
79
|
-
# Load the record
|
|
80
|
-
records = loader.load_connection_records(config[:query_name], options, self, config)
|
|
81
|
-
|
|
82
|
-
# Populate inverse cache if inverse_of is specified
|
|
83
|
-
populate_inverse_cache_for_connection(records, config, self)
|
|
84
|
-
|
|
85
|
-
# Cache it
|
|
86
|
-
@_connection_cache ||= {}
|
|
87
|
-
@_connection_cache[name] = records
|
|
88
|
-
records
|
|
89
|
-
elsif options.empty?
|
|
90
|
-
# If no runtime options are provided, reuse existing proxy if it exists
|
|
91
|
-
@_connection_proxies ||= {}
|
|
92
|
-
@_connection_proxies[name] ||= ConnectionProxy.new(
|
|
93
|
-
parent: self,
|
|
94
|
-
connection_name: name,
|
|
95
|
-
connection_config: self.class.connections[name],
|
|
96
|
-
options: options
|
|
97
|
-
)
|
|
98
|
-
else
|
|
99
|
-
# Create a new proxy for custom options (don't cache these)
|
|
100
|
-
ConnectionProxy.new(
|
|
101
|
-
parent: self,
|
|
102
|
-
connection_name: name,
|
|
103
|
-
connection_config: self.class.connections[name],
|
|
104
|
-
options: options
|
|
105
|
-
)
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Define setter method for testing/caching
|
|
110
|
-
define_method "#{name}=" do |value|
|
|
111
|
-
@_connection_cache ||= {}
|
|
112
|
-
@_connection_cache[name] = value
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# Load records with eager-loaded connections
|
|
117
|
-
# @param *connection_names [Symbol, Hash] The connection names to eager load
|
|
118
|
-
# @return [Class] A modified class for method chaining
|
|
119
|
-
#
|
|
120
|
-
# @example
|
|
121
|
-
# Customer.includes(:orders).find(123)
|
|
122
|
-
# Customer.includes(:orders, :addresses).where(email: "john@example.com")
|
|
123
|
-
# Order.includes(line_items: :variant)
|
|
124
|
-
def includes(*connection_names)
|
|
125
|
-
# Validate connections exist
|
|
126
|
-
validate_includes_connections!(connection_names)
|
|
127
|
-
|
|
128
|
-
# Collect connections with eager_load: true
|
|
129
|
-
auto_included_connections = connections.select { |_name, config| config[:eager_load] }.keys
|
|
130
|
-
|
|
131
|
-
# Merge manual and automatic connections
|
|
132
|
-
all_included_connections = (connection_names + auto_included_connections).uniq
|
|
133
|
-
|
|
134
|
-
# Create a scope object that holds the included connections
|
|
135
|
-
IncludesScope.new(self, all_included_connections)
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
private
|
|
139
|
-
|
|
140
|
-
def validate_inverse_of!(_name, _target_class_name, _inverse_name)
|
|
141
|
-
# Validation is deferred until runtime when connections are actually used
|
|
142
|
-
# This allows class definitions to be in any order
|
|
143
|
-
# The validation logic will be checked when inverse cache is populated
|
|
144
|
-
nil
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def validate_includes_connections!(connection_names)
|
|
148
|
-
connection_names.each do |name|
|
|
149
|
-
if name.is_a?(Hash)
|
|
150
|
-
name.each do |key, value|
|
|
151
|
-
raise ArgumentError, "Invalid connection for #{self.name}: #{key}. Available connections: #{connections.keys.join(', ')}" unless connections.key?(key.to_sym)
|
|
152
|
-
|
|
153
|
-
# Recursively validate nested connections
|
|
154
|
-
target_class = connections[key.to_sym][:class_name].constantize
|
|
155
|
-
if target_class.respond_to?(:validate_includes_connections!, true)
|
|
156
|
-
nested_names = value.is_a?(Array) ? value : [value]
|
|
157
|
-
target_class.send(:validate_includes_connections!, nested_names)
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
else
|
|
161
|
-
raise ArgumentError, "Invalid connection for #{self.name}: #{name}. Available connections: #{connections.keys.join(', ')}" unless connections.key?(name.to_sym)
|
|
162
|
-
end
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
# Instance method to populate inverse cache for lazy-loaded connections
|
|
168
|
-
|
|
169
|
-
def populate_inverse_cache_for_connection(records, connection_config, parent)
|
|
170
|
-
return unless connection_config[:inverse_of]
|
|
171
|
-
return if records.nil? || (records.is_a?(Array) && records.empty?)
|
|
172
|
-
|
|
173
|
-
inverse_name = connection_config[:inverse_of]
|
|
174
|
-
target_class = connection_config[:class_name].constantize
|
|
175
|
-
|
|
176
|
-
# Ensure target class has the inverse connection defined
|
|
177
|
-
return unless target_class.respond_to?(:connections) && target_class.connections[inverse_name]
|
|
178
|
-
|
|
179
|
-
inverse_type = target_class.connections[inverse_name][:type]
|
|
180
|
-
records_array = records.is_a?(Array) ? records : [records]
|
|
181
|
-
|
|
182
|
-
records_array.each do |record|
|
|
183
|
-
next unless record
|
|
184
|
-
|
|
185
|
-
record.instance_variable_set(:@_connection_cache, {}) unless record.instance_variable_get(:@_connection_cache)
|
|
186
|
-
cache = record.instance_variable_get(:@_connection_cache)
|
|
187
|
-
|
|
188
|
-
cache[inverse_name] =
|
|
189
|
-
if inverse_type == :singular
|
|
190
|
-
parent
|
|
191
|
-
else
|
|
192
|
-
# For collection inverses, wrap parent in an array
|
|
193
|
-
[parent]
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
end
|