Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 5 additions & 21 deletions lib/active_shopify_graphql/connections/connection_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@ def load_nested_connection(query_name, variables, parent, connection_config)
return nil if attributes.nil?

wire_inverse_of(parent, attributes, connection_config)
ModelBuilder.build(@context.model_class, attributes)
@context.model_class.new(attributes)
else
return [] if attributes.nil? || attributes.empty?

attributes.each { |attrs| wire_inverse_of(parent, attrs, connection_config) }
ModelBuilder.build_many(@context.model_class, attributes)
attributes.filter_map { |attrs| @context.model_class.new(attrs) if attrs }
end
end

Expand All @@ -87,33 +87,17 @@ def load_root_connection(query_name, variables, connection_config)
if singular
return nil if attributes.nil?

ModelBuilder.build(@context.model_class, attributes)
@context.model_class.new(attributes)
else
return [] if attributes.nil? || attributes.empty?

ModelBuilder.build_many(@context.model_class, attributes)
attributes.filter_map { |attrs| @context.model_class.new(attrs) if attrs }
end
end

# Wire up inverse_of associations by adding parent to attributes cache
def wire_inverse_of(parent, attributes, connection_config)
return unless attributes.is_a?(Hash) && connection_config&.dig(:inverse_of)

inverse_name = connection_config[:inverse_of]
attributes[:_connection_cache] ||= {}

# Check the type of the inverse connection
target_class = @context.model_class
return unless target_class.respond_to?(:connections) && target_class.connections[inverse_name]

inverse_type = target_class.connections[inverse_name][:type]
attributes[:_connection_cache][inverse_name] =
if inverse_type == :singular
parent
else
# For collection inverses, wrap parent in an array
[parent]
end
InverseCacheWiring.wire_attributes(attributes, connection_config, parent)
end

def extract_gid(parent)
Expand Down
24 changes: 2 additions & 22 deletions lib/active_shopify_graphql/connections/connection_proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -115,28 +115,8 @@ def populate_inverse_cache(records, connection_config, parent)
return unless connection_config[:inverse_of]
return if records.nil? || (records.is_a?(Array) && records.empty?)

inverse_name = connection_config[:inverse_of]
target_class = connection_config[:class_name].constantize

# Ensure target class has the inverse connection defined
return unless target_class.respond_to?(:connections) && target_class.connections[inverse_name]

inverse_type = target_class.connections[inverse_name][:type]
records_array = records.is_a?(Array) ? records : [records]

records_array.each do |record|
next unless record

record.instance_variable_set(:@_connection_cache, {}) unless record.instance_variable_get(:@_connection_cache)
cache = record.instance_variable_get(:@_connection_cache)

cache[inverse_name] =
if inverse_type == :singular
parent
else
# For collection inverses, wrap parent in an array
[parent]
end
Array(records).compact.each do |record|
InverseCacheWiring.wire_instance(record, connection_config, parent)
end
end
end
Expand Down
56 changes: 56 additions & 0 deletions lib/active_shopify_graphql/connections/inverse_cache_wiring.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

module ActiveShopifyGraphQL
module Connections
# Single home for the inverse cache wiring logic.
#
# When a connection has an +inverse_of+ option, loading records on one side
# should automatically populate the cache on the other side so the inverse
# association doesn't trigger an extra round-trip.
#
# Two entry points cover the two moments this happens:
#
# * +wire_instance+ — the child record is already a model instance (ivar-based cache).
# * +wire_attributes+ — the child record is still an attributes Hash, about to be
# instantiated (Hash-based cache, keyed +:_connection_cache+).
module InverseCacheWiring
# Wire the inverse cache on an already-built model instance.
#
# @param record [Object] a model instance that is the child side of the connection
# @param connection_config [Hash] the connection config hash that includes +:inverse_of+
# @param parent [Object] the parent model instance to store as the cached value
def self.wire_instance(record, connection_config, parent)
return unless record && connection_config[:inverse_of]

inverse_name = connection_config[:inverse_of]
target_class = connection_config[:class_name].constantize

return unless target_class.respond_to?(:connections) && target_class.connections[inverse_name]

inverse_type = target_class.connections[inverse_name][:type]

record.instance_variable_set(:@_connection_cache, {}) unless record.instance_variable_get(:@_connection_cache)
cache = record.instance_variable_get(:@_connection_cache)
cache[inverse_name] = inverse_type == :singular ? parent : [parent]
end

# Wire the inverse cache into an attributes Hash before model instantiation.
#
# @param attributes [Hash] the attributes hash that will be passed to +model_class.new+
# @param connection_config [Hash] the connection config hash that includes +:inverse_of+
# @param parent [Object] the parent model instance to store as the cached value
def self.wire_attributes(attributes, connection_config, parent)
return unless attributes.is_a?(Hash) && connection_config[:inverse_of]

inverse_name = connection_config[:inverse_of]
target_class = connection_config[:class_name].constantize

return unless target_class.respond_to?(:connections) && target_class.connections[inverse_name]

inverse_type = target_class.connections[inverse_name][:type]
attributes[:_connection_cache] ||= {}
attributes[:_connection_cache][inverse_name] = inverse_type == :singular ? parent : [parent]
end
end
end
end
10 changes: 9 additions & 1 deletion lib/active_shopify_graphql/loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ def initialization_args
[]
end

# Whether this loader can load a record without an explicit ID.
# Subclasses that support "current entity" semantics (e.g. Customer Account API)
# should override this to return true.
# @return [Boolean]
def supports_nil_id?
false
end

# Map the GraphQL response to model attributes
def map_response_to_attributes(response_data, parent_instance: nil)
mapper = Response::ResponseMapper.new(context)
Expand Down Expand Up @@ -88,7 +96,7 @@ def load_attributes(id)
# @param before [String, nil] Cursor to fetch records before
# @param sort_key [String, nil] The Shopify sort key (e.g., "CREATED_AT")
# @param reverse [Boolean, nil] Whether to reverse the sort order
# @param query_scope [Query::Scope] The query scope for navigation
# @param query_scope [Query::Relation] The query scope for navigation
# @return [PaginatedResult] A paginated result with attribute hashes and page info
def load_paginated_collection(conditions:, per_page:, query_scope:, after: nil, before: nil, sort_key: nil, reverse: nil)
collection_query_name = context.query_name.pluralize
Expand Down
14 changes: 7 additions & 7 deletions lib/active_shopify_graphql/loader_proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,17 @@ def find_by(conditions = {}, **options)
end

def find(id = nil)
# For Customer Account API, if no ID is provided, load the current customer
if id.nil? && @loader.is_a?(ActiveShopifyGraphQL::Loaders::CustomerAccountApiLoader)
# For loaders that support nil ID (e.g. Customer Account API current customer),
# delegate directly; otherwise require an ID via the standard flow.
if id.nil?
return nil unless @loader.supports_nil_id?

attributes = @loader.load_attributes
return nil if attributes.nil?
return @model_class.new(attributes) unless attributes.nil?

return @model_class.new(attributes)
return nil
end

# For other cases, require ID and use standard flow
return nil if id.nil?

build_relation.find(id)
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ def perform_graphql_query(query, **variables)
def initialization_args
[@token]
end

def supports_nil_id?
context.graphql_type == "Customer"
end
end
end
end
22 changes: 2 additions & 20 deletions lib/active_shopify_graphql/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,26 +52,8 @@ def populate_inverse_caches_on_initialization
connection_config = self.class.connections[connection_name]
next unless connection_config && connection_config[:inverse_of]

inverse_name = connection_config[:inverse_of]
records_array = Array(records).compact

records_array.each do |record|
next unless record.respond_to?(:instance_variable_set)

record.instance_variable_set(:@_connection_cache, {}) unless record.instance_variable_get(:@_connection_cache)
cache = record.instance_variable_get(:@_connection_cache)

# Determine the type of the inverse connection
next unless record.class.respond_to?(:connections) && record.class.connections[inverse_name]

inverse_type = record.class.connections[inverse_name][:type]
cache[inverse_name] =
if inverse_type == :singular
self
else
# For collection inverses, wrap parent in an array
[self]
end
Array(records).compact.each do |record|
ActiveShopifyGraphQL::Connections::InverseCacheWiring.wire_instance(record, connection_config, self)
end
end
end
Expand Down
24 changes: 2 additions & 22 deletions lib/active_shopify_graphql/model/connections.rb
Original file line number Diff line number Diff line change
Expand Up @@ -153,28 +153,8 @@ def populate_inverse_cache_for_connection(records, connection_config, parent)
return unless connection_config[:inverse_of]
return if records.nil? || (records.is_a?(Array) && records.empty?)

inverse_name = connection_config[:inverse_of]
target_class = connection_config[:class_name].constantize

# Ensure target class has the inverse connection defined
return unless target_class.respond_to?(:connections) && target_class.connections[inverse_name]

inverse_type = target_class.connections[inverse_name][:type]
records_array = records.is_a?(Array) ? records : [records]

records_array.each do |record|
next unless record

record.instance_variable_set(:@_connection_cache, {}) unless record.instance_variable_get(:@_connection_cache)
cache = record.instance_variable_get(:@_connection_cache)

cache[inverse_name] =
if inverse_type == :singular
parent
else
# For collection inverses, wrap parent in an array
[parent]
end
Array(records).compact.each do |record|
ActiveShopifyGraphQL::Connections::InverseCacheWiring.wire_instance(record, connection_config, parent)
end
end
end
14 changes: 2 additions & 12 deletions lib/active_shopify_graphql/model/graphql_type_resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def graphql_type_for_loader(loader_class)
return loader_type if loader_type

# 4. Infer from model name
return name.demodulize if respond_to?(:name) && name
return name.demodulize if name

raise NotImplementedError,
"#{self} must define graphql_type or #{loader_class} must define graphql_type"
Expand All @@ -57,17 +57,7 @@ def graphql_type_for_loader(loader_class)
# @param loader_class [Class, nil] The loader class
# @return [String] The resolved GraphQL type
def resolve_graphql_type(model_class: nil, loader_class: nil)
if model_class.respond_to?(:graphql_type_for_loader) && loader_class
model_class.graphql_type_for_loader(loader_class)
elsif model_class.respond_to?(:graphql_type)
model_class.graphql_type
elsif loader_class.respond_to?(:graphql_type)
loader_class.graphql_type
elsif model_class.respond_to?(:name) && model_class.name
model_class.name.demodulize
else
raise ArgumentError, "Cannot resolve graphql_type from provided arguments"
end
ActiveShopifyGraphQL::Model::GraphqlTypeResolver.resolve(model_class: model_class, loader_class: loader_class)
end
end

Expand Down
53 changes: 0 additions & 53 deletions lib/active_shopify_graphql/model_builder.rb

This file was deleted.

Loading
Loading