active_shopify_graphql 0.5.3 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e6fe91cae8b27f0c6154d8d43a42477e900dba59925b27421f5ea43278c8a786
4
- data.tar.gz: 6480859442d7a8ff30dc17fa3c0a96c5ae383f0ce0465fbbb8a67ff26c46f6da
3
+ metadata.gz: 5fbed890ba698aa5134ff32bdd471ffa8944e56eaf5d6a8a6d28e8060983e519
4
+ data.tar.gz: 34d83bf513460e154f33369fe54e953c68a466e37d16effccb7cb92014f16824
5
5
  SHA512:
6
- metadata.gz: e8572b7217ec7bb43172611d1d4059114b70de34342bb7c702265a90f3295004fd7de85b5449077054633ea199dd3f6606638f042cff3798dfc76d76dde24f4e
7
- data.tar.gz: 975eca8ec8d7ab7e3f166884baa6e885d76596d130f4525281b85075b3acef960f5562a45ed8a7ae73ac14e1a2e179cc129dbcff6a5b0d5588ba21ccb6838893
6
+ metadata.gz: 4a1dad259585ae87c1afa68c18eb03f74b48b13068b452a41a7d42bfb576c244df4de5eab321559dc5c9a5f11e1f2964fb0cdef142058ae345a75cb29e9e5700
7
+ data.tar.gz: f847bdf0f63e10b67f866abd6fae9c0d24195977d078c6d7125e7eff083f2f6ba99e1b0c30ada1374f5ceb4f36f0e8a1d52506a2d7354c7e01020d7d1ab4bcf7
data/README.md CHANGED
@@ -25,15 +25,21 @@ gem install active_shopify_graphql
25
25
  ```ruby
26
26
  # Configure in pure Ruby
27
27
  ActiveShopifyGraphQL.configure do |config|
28
- config.admin_api_client = ShopifyGraphQL::Client
29
- config.customer_account_client_class = Shopify::Account::Client
28
+ config.admin_api_executor = lambda do |query, **variables|
29
+ client = ShopifyAPI::Clients::Graphql::Admin.new(session: ShopifyAPI::Context.active_session)
30
+ response = client.query(query:, variables:)
31
+ response.body if response
32
+ end
30
33
  end
31
34
 
32
35
  # Or define a Rails initializer
33
36
  Rails.configuration.to_prepare do
34
37
  ActiveShopifyGraphQL.configure do |config|
35
- config.admin_api_client = ShopifyGraphQL::Client
36
- config.customer_account_client_class = Shopify::Account::Client
38
+ config.admin_api_executor = lambda do |query, **variables|
39
+ client = ShopifyAPI::Clients::Graphql::Admin.new(session: ShopifyAPI::Context.active_session)
40
+ response = client.query(query:, variables:)
41
+ response.body if response
42
+ end
37
43
  end
38
44
  end
39
45
 
@@ -90,7 +96,7 @@ orders = customer["orders"]["nodes"].map { |o| parse_order(o) }
90
96
  customer = Customer.includes(:orders).find(123456789)
91
97
  customer.email # => "john@example.com"
92
98
  customer.created_at # => #<DateTime>
93
- customer.orders.to_a # Lazily loaded as a single query
99
+ customer.orders # Eagerly loaded as a single query
94
100
  ```
95
101
 
96
102
  **Benefits:**
@@ -138,11 +144,19 @@ Configure your Shopify GraphQL clients:
138
144
  # config/initializers/active_shopify_graphql.rb
139
145
  Rails.configuration.to_prepare do
140
146
  ActiveShopifyGraphQL.configure do |config|
141
- # Admin API (must respond to #execute(query, **variables))
142
- config.admin_api_client = ShopifyGraphQL::Client
143
-
144
- # Customer Account API (must have .from_config(token) and #execute)
145
- config.customer_account_client_class = Shopify::Account::Client
147
+ # Admin API (receives query, **variables as arguments)
148
+ config.admin_api_executor = lambda do |query, **variables|
149
+ client = ShopifyAPI::Clients::Graphql::Admin.new(session: ShopifyAPI::Context.active_session)
150
+ response = client.query(query:, variables:)
151
+ response.body if response
152
+ end
153
+
154
+ # Optional Customer Account API (receives query, customer_token, **variables as arguments)
155
+ config.customer_account_api_executor = lambda do |query, customer_token, **variables|
156
+ # This is a custom client example
157
+ client = Shopify::Account::Client.from_config(customer_token)
158
+ response = client.query(query, **variables)
159
+ end
146
160
  end
147
161
  end
148
162
  ```
@@ -309,7 +323,8 @@ Customer.where(email: "john@example.com")
309
323
  Customer.where(created_at: { gte: "2024-01-01", lt: "2024-02-01" })
310
324
  Customer.where(orders_count: { gte: 5 })
311
325
 
312
- # Wildcards (string query)
326
+ # Wildcards are supported in string queries only
327
+ # Use with caution as interpolation can lead to security issues
313
328
  Customer.where("email:*@example.com")
314
329
 
315
330
  # Parameter binding (safe)
@@ -319,6 +334,21 @@ Customer.where("email::email", email: "john@example.com")
319
334
  Customer.where(email: "@gmail.com").limit(100)
320
335
  ```
321
336
 
337
+ #### Sorting
338
+
339
+ ```ruby
340
+ # Sort by created_at ascending (default)
341
+ Customer.where(email: "@example.com").order(sort_key: "CREATED_AT")
342
+
343
+ # Sort by updated_at descending
344
+ Customer.order(sort_key: "UPDATED_AT", reverse: true)
345
+
346
+ # Combine with other chainable methods
347
+ Customer.where(country: "Canada").order(sort_key: "CREATED_AT").limit(25)
348
+ ```
349
+
350
+ **Note:** Sort keys are Shopify GraphQL enum values (e.g., `"CREATED_AT"`, `"UPDATED_AT"`, `"RELEVANCE"`). Available values depend on the specific Shopify API endpoint.
351
+
322
352
  #### Query Optimization
323
353
 
324
354
  ```ruby
@@ -349,7 +379,7 @@ ProductVariant.where("sku:FRZ*").in_pages(of: 10) do |page|
349
379
  end
350
380
 
351
381
  # Lazy enumeration
352
- scope = Customer.where(email: "*@example.com")
382
+ scope = Customer.where(email: "@example.com")
353
383
  scope.each { |c| puts c.name } # Executes query
354
384
  scope.first # Fetches just first
355
385
  ```
@@ -3,11 +3,11 @@
3
3
  module ActiveShopifyGraphQL
4
4
  # Configuration class for setting up external dependencies
5
5
  class Configuration
6
- attr_accessor :admin_api_client, :customer_account_client_class, :logger, :log_queries, :max_objects_per_paginated_query
6
+ attr_accessor :admin_api_executor, :customer_account_api_executor, :logger, :log_queries, :max_objects_per_paginated_query
7
7
 
8
8
  def initialize
9
- @admin_api_client = nil
10
- @customer_account_client_class = nil
9
+ @admin_api_executor = nil
10
+ @customer_account_api_executor = nil
11
11
  @logger = nil
12
12
  @log_queries = false
13
13
  @max_objects_per_paginated_query = 250
@@ -44,7 +44,7 @@ module ActiveShopifyGraphQL
44
44
  )
45
45
 
46
46
  parent_id = extract_gid(parent)
47
- response_data = @loader_instance.perform_graphql_query(query, id: parent_id)
47
+ response_data = @loader_instance.execute_query(query, id: parent_id)
48
48
 
49
49
  return [] if response_data.nil?
50
50
 
@@ -76,7 +76,7 @@ module ActiveShopifyGraphQL
76
76
  singular: singular
77
77
  )
78
78
 
79
- response_data = @loader_instance.perform_graphql_query(query)
79
+ response_data = @loader_instance.execute_query(query)
80
80
 
81
81
  return [] if response_data.nil?
82
82
 
@@ -58,7 +58,7 @@ module ActiveShopifyGraphQL
58
58
 
59
59
  # Map the GraphQL response to model attributes
60
60
  def map_response_to_attributes(response_data, parent_instance: nil)
61
- mapper = create_response_mapper
61
+ mapper = Response::ResponseMapper.new(context)
62
62
  attributes = mapper.map_response(response_data)
63
63
  cache_connections(mapper, response_data, target: attributes, parent_instance: parent_instance)
64
64
  attributes
@@ -74,7 +74,7 @@ module ActiveShopifyGraphQL
74
74
  # @return [Hash, nil] Attribute hash with connection cache, or nil if not found
75
75
  def load_attributes(id)
76
76
  query = Query::QueryBuilder.build_single_record_query(context)
77
- response_data = execute_query(query, id: id)
77
+ response_data = perform_graphql_query(query, id: id)
78
78
  return nil if response_data.nil?
79
79
 
80
80
  map_response_to_attributes(response_data)
@@ -86,15 +86,19 @@ module ActiveShopifyGraphQL
86
86
  # @param per_page [Integer] Number of records per page
87
87
  # @param after [String, nil] Cursor to fetch records after
88
88
  # @param before [String, nil] Cursor to fetch records before
89
+ # @param sort_key [String, nil] The Shopify sort key (e.g., "CREATED_AT")
90
+ # @param reverse [Boolean, nil] Whether to reverse the sort order
89
91
  # @param query_scope [Query::Scope] The query scope for navigation
90
92
  # @return [PaginatedResult] A paginated result with attribute hashes and page info
91
- def load_paginated_collection(conditions:, per_page:, query_scope:, after: nil, before: nil)
93
+ def load_paginated_collection(conditions:, per_page:, query_scope:, after: nil, before: nil, sort_key: nil, reverse: nil)
92
94
  collection_query_name = context.query_name.pluralize
93
95
  variables = build_collection_variables(
94
96
  conditions,
95
97
  per_page: per_page,
96
98
  after: after,
97
- before: before
99
+ before: before,
100
+ sort_key: sort_key,
101
+ reverse: reverse
98
102
  )
99
103
 
100
104
  query = Query::QueryBuilder.build_paginated_collection_query(
@@ -113,6 +117,11 @@ module ActiveShopifyGraphQL
113
117
  connection_loader.load_records(query_name, variables, parent, connection_config)
114
118
  end
115
119
 
120
+ def execute_query(query, **variables)
121
+ log_query(self.class.name, query, variables)
122
+ perform_graphql_query(query, **variables)
123
+ end
124
+
116
125
  # Abstract method for executing GraphQL queries
117
126
  def perform_graphql_query(query, **variables)
118
127
  raise NotImplementedError, "#{self.class} must implement perform_graphql_query"
@@ -120,10 +129,6 @@ module ActiveShopifyGraphQL
120
129
 
121
130
  private
122
131
 
123
- def create_response_mapper
124
- Response::ResponseMapper.new(context)
125
- end
126
-
127
132
  def should_log?
128
133
  ActiveShopifyGraphQL.configuration.log_queries && ActiveShopifyGraphQL.configuration.logger
129
134
  end
@@ -175,17 +180,13 @@ module ActiveShopifyGraphQL
175
180
  raise ArgumentError, "Shopify query validation failed: #{messages.join(', ')}"
176
181
  end
177
182
 
178
- def execute_query(query, **variables)
179
- perform_graphql_query(query, **variables)
180
- end
181
-
182
183
  def execute_query_and_validate_search_response(query, **variables)
183
184
  response = execute_query(query, **variables)
184
185
  validate_search_query_response(response)
185
186
  response
186
187
  end
187
188
 
188
- def build_collection_variables(conditions, per_page:, after: nil, before: nil)
189
+ def build_collection_variables(conditions, per_page:, after: nil, before: nil, sort_key: nil, reverse: nil)
189
190
  search_query = SearchQuery.new(conditions)
190
191
  variables = { query: search_query.to_s }
191
192
 
@@ -197,6 +198,9 @@ module ActiveShopifyGraphQL
197
198
  variables[:after] = after if after
198
199
  end
199
200
 
201
+ variables[:sort_key] = sort_key if sort_key
202
+ variables[:reverse] = reverse unless reverse.nil?
203
+
200
204
  variables.compact
201
205
  end
202
206
 
@@ -4,12 +4,10 @@ module ActiveShopifyGraphQL
4
4
  module Loaders
5
5
  class AdminApiLoader < Loader
6
6
  def perform_graphql_query(query, **variables)
7
- log_query("Admin API", query, variables)
7
+ executor = ActiveShopifyGraphQL.configuration.admin_api_executor
8
+ raise Error, "Admin API executor not configured. Please configure it using ActiveShopifyGraphQL.configure" unless executor
8
9
 
9
- client = ActiveShopifyGraphQL.configuration.admin_api_client
10
- raise Error, "Admin API client not configured. Please configure it using ActiveShopifyGraphQL.configure" unless client
11
-
12
- client.execute(query, **variables)
10
+ executor.call(query, **variables)
13
11
  end
14
12
  end
15
13
  end
@@ -23,16 +23,11 @@ module ActiveShopifyGraphQL
23
23
  map_response_to_attributes(response_data)
24
24
  end
25
25
 
26
- def client
27
- client_class = ActiveShopifyGraphQL.configuration.customer_account_client_class
28
- raise Error, "Customer Account API client class not configured" unless client_class
29
-
30
- @client ||= client_class.from_config(@token)
31
- end
32
-
33
26
  def perform_graphql_query(query, **variables)
34
- log_query("Customer Account API", query, variables)
35
- client.query(query, variables)
27
+ executor = ActiveShopifyGraphQL.configuration.customer_account_api_executor
28
+ raise Error, "Customer Account API executor not configured. Please configure it using ActiveShopifyGraphQL.configure" unless executor
29
+
30
+ executor.call(query, @token, **variables)
36
31
  end
37
32
 
38
33
  def initialization_args
@@ -102,7 +102,7 @@ module ActiveShopifyGraphQL::Model::Connections
102
102
  )
103
103
  else
104
104
  # Create a new proxy for custom options (don't cache these)
105
- Connections::ConnectionProxy.new(
105
+ ActiveShopifyGraphQL::Connections::ConnectionProxy.new(
106
106
  parent: self,
107
107
  connection_name: name,
108
108
  connection_config: self.class.connections[name],
@@ -120,6 +120,18 @@ module ActiveShopifyGraphQL::Model::FinderMethods
120
120
  all.includes(*connection_names)
121
121
  end
122
122
 
123
+ # Set the sort order for the query
124
+ # @param sort_key [String] The Shopify sort key (e.g., "CREATED_AT", "UPDATED_AT")
125
+ # @param reverse [Boolean] Whether to reverse the sort order (optional)
126
+ # @return [Relation] A relation with order applied
127
+ #
128
+ # @example
129
+ # Customer.order(sort_key: "CREATED_AT").where(country: "Canada")
130
+ # Customer.where(status: "active").order(sort_key: "UPDATED_AT", reverse: true)
131
+ def order(sort_key:, reverse: nil)
132
+ all.order(sort_key: sort_key, reverse: reverse)
133
+ end
134
+
123
135
  private
124
136
 
125
137
  # Validates that selected attributes exist in the model
@@ -23,7 +23,7 @@ module ActiveShopifyGraphQL
23
23
 
24
24
  DEFAULT_PER_PAGE = 250
25
25
 
26
- attr_reader :model_class, :included_connections, :conditions, :total_limit, :per_page
26
+ attr_reader :model_class, :included_connections, :conditions, :total_limit, :per_page, :sort_key, :reverse
27
27
 
28
28
  def initialize(
29
29
  model_class,
@@ -33,7 +33,9 @@ module ActiveShopifyGraphQL
33
33
  total_limit: nil,
34
34
  per_page: DEFAULT_PER_PAGE,
35
35
  loader_class: nil,
36
- loader_extra_args: []
36
+ loader_extra_args: [],
37
+ sort_key: nil,
38
+ reverse: nil
37
39
  )
38
40
  @model_class = model_class
39
41
  @conditions = conditions
@@ -43,6 +45,8 @@ module ActiveShopifyGraphQL
43
45
  @per_page = [per_page, ActiveShopifyGraphQL.configuration.max_objects_per_paginated_query].min
44
46
  @loader_class = loader_class
45
47
  @loader_extra_args = loader_extra_args
48
+ @sort_key = sort_key
49
+ @reverse = reverse
46
50
  @loaded = false
47
51
  @records = nil
48
52
  end
@@ -66,6 +70,22 @@ module ActiveShopifyGraphQL
66
70
  spawn(conditions: new_conditions)
67
71
  end
68
72
 
73
+ # Set the sort order for the query
74
+ # @param sort_key [String] The Shopify sort key (e.g., "CREATED_AT", "UPDATED_AT")
75
+ # @param reverse [Boolean] Whether to reverse the sort order (optional)
76
+ # @return [Relation] A new relation with order applied
77
+ # @raise [ArgumentError] If order is called on a relation that already has ordering
78
+ # @example
79
+ # Customer.where(email: "*@example.com").order(sort_key: "CREATED_AT", reverse: true)
80
+ def order(sort_key:, reverse: nil)
81
+ if has_ordering?
82
+ raise ArgumentError, "Chaining multiple order clauses is not supported. " \
83
+ "Combine ordering in a single order call instead."
84
+ end
85
+
86
+ spawn(sort_key: sort_key, reverse: reverse)
87
+ end
88
+
69
89
  # Find a single record by conditions
70
90
  # @param conditions [Hash] The conditions to match
71
91
  # @return [Object, nil] The first matching record or nil
@@ -302,6 +322,8 @@ module ActiveShopifyGraphQL
302
322
  per_page: effective_per_page,
303
323
  after: after,
304
324
  before: before,
325
+ sort_key: @sort_key,
326
+ reverse: @reverse,
305
327
  query_scope: build_query_scope_for_pagination
306
328
  )
307
329
  end
@@ -324,7 +346,9 @@ module ActiveShopifyGraphQL
324
346
  total_limit: changes.fetch(:total_limit, @total_limit),
325
347
  per_page: changes.fetch(:per_page, @per_page),
326
348
  loader_class: changes.fetch(:loader_class, @loader_class),
327
- loader_extra_args: changes.fetch(:loader_extra_args, @loader_extra_args)
349
+ loader_extra_args: changes.fetch(:loader_extra_args, @loader_extra_args),
350
+ sort_key: changes.fetch(:sort_key, @sort_key),
351
+ reverse: changes.fetch(:reverse, @reverse)
328
352
  )
329
353
  end
330
354
 
@@ -341,6 +365,10 @@ module ActiveShopifyGraphQL
341
365
  end
342
366
  end
343
367
 
368
+ def has_ordering?
369
+ !@sort_key.nil?
370
+ end
371
+
344
372
  def build_loader
345
373
  klass = @loader_class || @model_class.send(:default_loader_class)
346
374
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveShopifyGraphQL
4
- VERSION = "0.5.3"
4
+ VERSION = "1.0.0"
5
5
  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.5.3
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicolò Rebughini