Class: ActiveShopifyGraphQL::Query::Relation

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/active_shopify_graphql/query/relation.rb

Overview

A unified query builder that encapsulates all query configuration. This class provides a consistent interface for chaining operations like where, find_by, includes, select, limit, and pagination.

Inspired by ActiveRecord::Relation, this class accumulates query state and executes the query when records are accessed.

Examples:

Basic usage

Customer.where(email: "[email protected]").first
Customer.includes(:orders).find_by(id: 123)
Customer.select(:id, :email).where(first_name: "John").limit(10).to_a

Pagination

Customer.where(country: "Canada").in_pages(of: 50) do |page|
  page.each { |customer| process(customer) }
end

Constant Summary collapse

DEFAULT_PER_PAGE =
250

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model_class, conditions: {}, included_connections: [], selected_attributes: nil, total_limit: nil, per_page: DEFAULT_PER_PAGE, loader_class: nil, loader_extra_args: [], sort_key: nil, reverse: nil) ⇒ Relation

Returns a new instance of Relation.



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/active_shopify_graphql/query/relation.rb', line 28

def initialize(
  model_class,
  conditions: {},
  included_connections: [],
  selected_attributes: nil,
  total_limit: nil,
  per_page: DEFAULT_PER_PAGE,
  loader_class: nil,
  loader_extra_args: [],
  sort_key: nil,
  reverse: nil
)
  @model_class = model_class
  @conditions = conditions
  @included_connections = included_connections
  @selected_attributes = selected_attributes
  @total_limit = total_limit
  @per_page = [per_page, ActiveShopifyGraphQL.configuration.max_objects_per_paginated_query].min
  @loader_class = loader_class
  @loader_extra_args = loader_extra_args
  @sort_key = sort_key
  @reverse = reverse
  @loaded = false
  @records = nil
end

Instance Attribute Details

#conditionsObject (readonly)

Returns the value of attribute conditions.



26
27
28
# File 'lib/active_shopify_graphql/query/relation.rb', line 26

def conditions
  @conditions
end

#included_connectionsObject (readonly)

Returns the value of attribute included_connections.



26
27
28
# File 'lib/active_shopify_graphql/query/relation.rb', line 26

def included_connections
  @included_connections
end

#model_classObject (readonly)

Returns the value of attribute model_class.



26
27
28
# File 'lib/active_shopify_graphql/query/relation.rb', line 26

def model_class
  @model_class
end

#per_pageObject (readonly)

Returns the value of attribute per_page.



26
27
28
# File 'lib/active_shopify_graphql/query/relation.rb', line 26

def per_page
  @per_page
end

#reverseObject (readonly)

Returns the value of attribute reverse.



26
27
28
# File 'lib/active_shopify_graphql/query/relation.rb', line 26

def reverse
  @reverse
end

#sort_keyObject (readonly)

Returns the value of attribute sort_key.



26
27
28
# File 'lib/active_shopify_graphql/query/relation.rb', line 26

def sort_key
  @sort_key
end

#total_limitObject (readonly)

Returns the value of attribute total_limit.



26
27
28
# File 'lib/active_shopify_graphql/query/relation.rb', line 26

def total_limit
  @total_limit
end

Instance Method Details

#[](index) ⇒ Object

Array-like access



276
277
278
# File 'lib/active_shopify_graphql/query/relation.rb', line 276

def [](index)
  to_a[index]
end

#countInteger

Count records (loads all pages)

Returns:



271
272
273
# File 'lib/active_shopify_graphql/query/relation.rb', line 271

def count
  to_a.count
end

#each {|Object| ... } ⇒ Object

Iterate through all records across all pages

Yields:

  • (Object)

    Each record



216
217
218
219
220
221
222
# File 'lib/active_shopify_graphql/query/relation.rb', line 216

def each(&block)
  return to_enum(:each) unless block_given?

  each_page do |page|
    page.each(&block)
  end
end

#each_page {|PaginatedResult| ... } ⇒ Object

Iterate through all pages, yielding each page

Yields:

  • (PaginatedResult)

    Each page of results



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/active_shopify_graphql/query/relation.rb', line 178

def each_page
  current_page = fetch_first_page
  records_yielded = 0

  loop do
    break if current_page.empty?

    # Apply total limit if set
    if @total_limit
      remaining = @total_limit - records_yielded
      break if remaining <= 0

      if current_page.size > remaining
        trimmed_records = current_page.records.first(remaining)
        current_page = PaginatedResult.new(
          records: trimmed_records,
          page_info: PageInfo.new,
          query_scope: build_query_scope_for_pagination
        )
      end
    end

    yield current_page
    records_yielded += current_page.size

    break unless current_page.has_next_page?
    break if @total_limit && records_yielded >= @total_limit

    current_page = current_page.next_page
  end
end

#empty?Boolean

Check if no records exist

Returns:



258
259
260
# File 'lib/active_shopify_graphql/query/relation.rb', line 258

def empty?
  first(1).empty?
end

#exists?Boolean

Check if any records exist

Returns:



252
253
254
# File 'lib/active_shopify_graphql/query/relation.rb', line 252

def exists?
  first(1).any?
end

#fetch_first_pagePaginatedResult

Fetch the first page of results

Returns:



333
334
335
# File 'lib/active_shopify_graphql/query/relation.rb', line 333

def fetch_first_page
  fetch_page
end

#fetch_page(after: nil, before: nil) ⇒ PaginatedResult

Fetch a specific page by cursor

Parameters:

  • (defaults to: nil)

    Cursor to fetch records after

  • (defaults to: nil)

    Cursor to fetch records before

Returns:



319
320
321
322
323
324
325
326
327
328
329
# File 'lib/active_shopify_graphql/query/relation.rb', line 319

def fetch_page(after: nil, before: nil)
  loader.load_paginated_collection(
    conditions: @conditions,
    per_page: effective_per_page,
    after: after,
    before: before,
    sort_key: @sort_key,
    reverse: @reverse,
    query_scope: build_query_scope_for_pagination
  )
end

#find(id = nil) ⇒ Object

Find a single record by ID For Customer Account API, if no ID is provided, fetches the current customer

Parameters:

  • (defaults to: nil)

    The record ID (will be converted to GID automatically)

Returns:

  • The model instance

Raises:

  • If the record is not found

  • If id is nil and not using Customer Account API loader



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/active_shopify_graphql/query/relation.rb', line 103

def find(id = nil)
  # Handle Customer Account API case where no ID means "current customer"
  if id.nil?
    raise ArgumentError, "find requires an ID argument unless using Customer Account API" unless loader.is_a?(ActiveShopifyGraphQL::Loaders::CustomerAccountApiLoader)

    attributes = loader.load_attributes
    raise ObjectNotFoundError, "Couldn't find current customer" if attributes.nil?

    return ModelBuilder.build(@model_class, attributes)
  end

  # Standard case: find by ID
  gid = GidHelper.normalize_gid(id, @model_class.graphql_type)
  attributes = loader.load_attributes(gid)

  raise ObjectNotFoundError, "Couldn't find #{@model_class.name} with id=#{id}" if attributes.nil?

  ModelBuilder.build(@model_class, attributes)
end

#find_by(conditions = {}, **options) ⇒ Object?

Find a single record by conditions

Parameters:

  • (defaults to: {})

    The conditions to match

Returns:

  • The first matching record or nil



92
93
94
95
# File 'lib/active_shopify_graphql/query/relation.rb', line 92

def find_by(conditions = {}, **options)
  merged = conditions.empty? ? options : conditions
  where(merged).first
end

#first(count = nil) ⇒ Object, ...

Get first record(s)

Parameters:

  • (defaults to: nil)

    Number of records to return

Returns:

  • First record(s) or nil



242
243
244
245
246
247
248
# File 'lib/active_shopify_graphql/query/relation.rb', line 242

def first(count = nil)
  if count
    spawn(total_limit: count, per_page: [count, ActiveShopifyGraphQL.configuration.max_objects_per_paginated_query].min).to_a
  else
    spawn(total_limit: 1, per_page: 1).to_a.first
  end
end

#has_included_connections?Boolean


Internal State Accessors (for compatibility)

Returns:



307
308
309
# File 'lib/active_shopify_graphql/query/relation.rb', line 307

def has_included_connections?
  @included_connections.any?
end

#in_pages(of: DEFAULT_PER_PAGE) {|PaginatedResult| ... } ⇒ PaginatedResult, self

Configure pagination and optionally iterate through pages

Parameters:

  • (defaults to: DEFAULT_PER_PAGE)

    Records per page (default: 250, max: configurable)

Yields:

  • (PaginatedResult)

    Each page of results

Returns:

  • PaginatedResult if no block given



164
165
166
167
168
169
170
171
172
173
174
# File 'lib/active_shopify_graphql/query/relation.rb', line 164

def in_pages(of: DEFAULT_PER_PAGE, &block)
  page_size = [of, ActiveShopifyGraphQL.configuration.max_objects_per_paginated_query].min
  scoped = spawn(per_page: page_size)

  if block_given?
    scoped.each_page(&block)
    self
  else
    scoped.fetch_first_page
  end
end

#includes(*connection_names) ⇒ Relation

Include connections for eager loading

Parameters:

  • Connection names to include

Returns:

  • A new relation with connections included



126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/active_shopify_graphql/query/relation.rb', line 126

def includes(*connection_names)
  validate_includes_connections!(connection_names)

  # Merge with existing and auto-eager-loaded connections
  auto_included = @model_class.connections
                              .select { |_name, config| config[:eager_load] }
                              .keys

  all_connections = (@included_connections + connection_names + auto_included).uniq

  spawn(included_connections: all_connections)
end

#inspectObject


Inspection



294
295
296
297
298
299
300
301
# File 'lib/active_shopify_graphql/query/relation.rb', line 294

def inspect
  parts = [@model_class.name]
  parts << "includes(#{@included_connections.join(', ')})" if @included_connections.any?
  parts << "select(#{@selected_attributes.join(', ')})" if @selected_attributes
  parts << "where(#{@conditions.inspect})" unless @conditions.empty?
  parts << "limit(#{@total_limit})" if @total_limit
  "#<#{self.class.name} #{parts.join('.')}>"
end

#limit(count) ⇒ Relation

Limit the total number of records returned

Parameters:

  • Maximum records to return

Returns:

  • A new relation with limit applied



152
153
154
# File 'lib/active_shopify_graphql/query/relation.rb', line 152

def limit(count)
  spawn(total_limit: count)
end

#map(&block) ⇒ Object

Map over records



281
282
283
# File 'lib/active_shopify_graphql/query/relation.rb', line 281

def map(&block)
  to_a.map(&block)
end

#order(sort_key:, reverse: nil) ⇒ Relation

Set the sort order for the query

Examples:

Customer.where(email: "*@example.com").order(sort_key: "CREATED_AT", reverse: true)

Parameters:

  • The Shopify sort key (e.g., "CREATED_AT", "UPDATED_AT")

  • (defaults to: nil)

    Whether to reverse the sort order (optional)

Returns:

  • A new relation with order applied

Raises:

  • If order is called on a relation that already has ordering



80
81
82
83
84
85
86
87
# File 'lib/active_shopify_graphql/query/relation.rb', line 80

def order(sort_key:, reverse: nil)
  if has_ordering?
    raise ArgumentError, "Chaining multiple order clauses is not supported. " \
                         "Combine ordering in a single order call instead."
  end

  spawn(sort_key: sort_key, reverse: reverse)
end

#select(*attributes) ⇒ Relation

Select specific attributes to optimize the query

Parameters:

  • Attributes to select

Returns:

  • A new relation with selected attributes



142
143
144
145
146
147
# File 'lib/active_shopify_graphql/query/relation.rb', line 142

def select(*attributes)
  attrs = Array(attributes).flatten.map(&:to_sym)
  validate_select_attributes!(attrs)

  spawn(selected_attributes: attrs)
end

#select_records(&block) ⇒ Object

Select/filter records (Array compatibility - differs from query select)



286
287
288
# File 'lib/active_shopify_graphql/query/relation.rb', line 286

def select_records(&block)
  to_a.select(&block)
end

#sizeInteger Also known as: length

Size/length of records (loads all pages)

Returns:



264
265
266
# File 'lib/active_shopify_graphql/query/relation.rb', line 264

def size
  to_a.size
end

#to_aArray Also known as: load

Load all records respecting total_limit

Returns:

  • All records



226
227
228
229
230
231
232
233
234
235
236
# File 'lib/active_shopify_graphql/query/relation.rb', line 226

def to_a
  return @records if @loaded

  all_records = []
  each_page do |page|
    all_records.concat(page.to_a)
  end
  @records = all_records
  @loaded = true
  @records
end

#where(conditions_or_first_condition = {}, *args, **options) ⇒ Relation

Add conditions to the query

Parameters:

  • (defaults to: {})

    Conditions to filter by

Returns:

  • A new relation with conditions applied

Raises:

  • If where is called on a relation that already has conditions



62
63
64
65
66
67
68
69
70
71
# File 'lib/active_shopify_graphql/query/relation.rb', line 62

def where(conditions_or_first_condition = {}, *args, **options)
  new_conditions = build_conditions(conditions_or_first_condition, args, options)

  if has_conditions? && !new_conditions.empty?
    raise ArgumentError, "Chaining multiple where clauses is not supported. " \
                         "Combine conditions in a single where call instead."
  end

  spawn(conditions: new_conditions)
end