active_shopify_graphql 0.4.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 +3 -0
- data/README.md +158 -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/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 +29 -29
- metadata +46 -15
- data/lib/active_shopify_graphql/associations.rb +0 -94
- 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 -182
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3044e5a4ac6255ea55f7d10e48b0fe9a65825f813712f0cdc14e5993437e24bf
|
|
4
|
+
data.tar.gz: f6b9e14fe814887bf0af4b90c174088f7c912ead322cb353fd84a95b2c4799e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d5b38dbe06885fc9783e0f0980f1c690ad0a3edc0355f379bff6c0277eb0d69c76c07824d31295a020272b648ee8cd90c812e7399c75dc629650a9681df04b2f
|
|
7
|
+
data.tar.gz: 6bdfb30c5888e3532cd19325769d460470c044d5db9c47ebf4b52bec368e8baaccfc65660d2696e04f978bf3d5d178e15a914718925a82b08f655fea8a3d25bf
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
|
@@ -52,12 +52,10 @@ end
|
|
|
52
52
|
|
|
53
53
|
### Basic Model Setup
|
|
54
54
|
|
|
55
|
-
Create a model that
|
|
55
|
+
Create a model that inherits from `ActiveShopifyGraphQL::Model` and define attributes directly:
|
|
56
56
|
|
|
57
57
|
```ruby
|
|
58
|
-
class Customer
|
|
59
|
-
include ActiveShopifyGraphQL::Base
|
|
60
|
-
|
|
58
|
+
class Customer < ActiveShopifyGraphQL::Model
|
|
61
59
|
# Define the GraphQL type
|
|
62
60
|
graphql_type "Customer"
|
|
63
61
|
|
|
@@ -75,6 +73,37 @@ class Customer
|
|
|
75
73
|
end
|
|
76
74
|
```
|
|
77
75
|
|
|
76
|
+
### Application Base Class (Recommended)
|
|
77
|
+
|
|
78
|
+
For consistency and to share common behavior across all your Shopify GraphQL models, we recommend creating an `ApplicationShopifyGqlRecord` base class, similar to Rails' `ApplicationRecord`:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
# app/models/application_shopify_gql_record.rb
|
|
82
|
+
class ApplicationShopifyRecord < ActiveShopifyGraphQL::Model
|
|
83
|
+
# Extract numeric ID from Shopify GID
|
|
84
|
+
attribute :id, transform: ->(id) { id.split("/").last }
|
|
85
|
+
# Keep the original GID available
|
|
86
|
+
attribute :gid, path: "id"
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Then inherit from this base class in your models:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
class Customer < ApplicationShopifyRecord
|
|
94
|
+
graphql_type "Customer"
|
|
95
|
+
|
|
96
|
+
attribute :name, path: "displayName"
|
|
97
|
+
attribute :email, path: "defaultEmailAddress.emailAddress"
|
|
98
|
+
attribute :created_at, type: :datetime
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
This pattern provides:
|
|
103
|
+
- **Consistent ID handling**: All models automatically get a numeric `id` and full `gid`
|
|
104
|
+
- **Shared behavior**: Add validations, methods, or transformations once for all models
|
|
105
|
+
- **Clear inheritance**: The class definition line clearly shows the persistence layer
|
|
106
|
+
|
|
78
107
|
### Defining Attributes
|
|
79
108
|
|
|
80
109
|
Attributes are now defined directly in the model class using the `attribute` method. The GraphQL fragments and response mapping are automatically generated!
|
|
@@ -82,9 +111,7 @@ Attributes are now defined directly in the model class using the `attribute` met
|
|
|
82
111
|
#### Basic Attribute Definition
|
|
83
112
|
|
|
84
113
|
```ruby
|
|
85
|
-
class Customer
|
|
86
|
-
include ActiveShopifyGraphQL::Base
|
|
87
|
-
|
|
114
|
+
class Customer < ActiveShopifyGraphQL::Model
|
|
88
115
|
graphql_type "Customer"
|
|
89
116
|
|
|
90
117
|
# Define attributes with automatic GraphQL path inference and type coercion
|
|
@@ -124,9 +151,7 @@ attribute :name,
|
|
|
124
151
|
Shopify metafields can be easily accessed using the `metafield_attribute` method:
|
|
125
152
|
|
|
126
153
|
```ruby
|
|
127
|
-
class Product
|
|
128
|
-
include ActiveShopifyGraphQL::Base
|
|
129
|
-
|
|
154
|
+
class Product < ActiveShopifyGraphQL::Model
|
|
130
155
|
graphql_type "Product"
|
|
131
156
|
|
|
132
157
|
# Regular attributes
|
|
@@ -148,9 +173,7 @@ The metafield attributes automatically generate the correct GraphQL syntax and h
|
|
|
148
173
|
For advanced GraphQL features not yet fully supported by the gem (like union types with `... on` syntax), you can inject raw GraphQL directly into the query using the `raw_graphql` option:
|
|
149
174
|
|
|
150
175
|
```ruby
|
|
151
|
-
class Product
|
|
152
|
-
include ActiveShopifyGraphQL::Base
|
|
153
|
-
|
|
176
|
+
class Product < ActiveShopifyGraphQL::Model
|
|
154
177
|
graphql_type "Product"
|
|
155
178
|
|
|
156
179
|
attribute :id, type: :string
|
|
@@ -166,7 +189,7 @@ class Product
|
|
|
166
189
|
attribute :product_bundle,
|
|
167
190
|
path: "bundle", # The alias will be used as the response key
|
|
168
191
|
type: :json,
|
|
169
|
-
raw_graphql: 'metafield(namespace: "bundles", key: "items") { references(first: 10) {
|
|
192
|
+
raw_graphql: 'metafield(namespace: "bundles", key: "items") { references(first: 10) { nodes { ... on Product { id title } } } }'
|
|
170
193
|
end
|
|
171
194
|
```
|
|
172
195
|
|
|
@@ -175,9 +198,7 @@ end
|
|
|
175
198
|
For models that need different attributes depending on the API being used, you can define loader-specific overrides:
|
|
176
199
|
|
|
177
200
|
```ruby
|
|
178
|
-
class Customer
|
|
179
|
-
include ActiveShopifyGraphQL::Base
|
|
180
|
-
|
|
201
|
+
class Customer < ActiveShopifyGraphQL::Model
|
|
181
202
|
graphql_type "Customer"
|
|
182
203
|
|
|
183
204
|
# Default attributes (used by all loaders)
|
|
@@ -234,7 +255,7 @@ customer = Customer.with_loader(MyCustomLoader).find(id)
|
|
|
234
255
|
Use the `where` method to query multiple records using Shopify's search syntax:
|
|
235
256
|
|
|
236
257
|
```ruby
|
|
237
|
-
#
|
|
258
|
+
# Hash-based queries (safe, with automatic escaping)
|
|
238
259
|
customers = Customer.where(email: "john@example.com")
|
|
239
260
|
|
|
240
261
|
# Range queries
|
|
@@ -248,8 +269,106 @@ customers = Customer.where(first_name: "John Doe")
|
|
|
248
269
|
customers = Customer.where({ email: "john@example.com" }, limit: 100)
|
|
249
270
|
```
|
|
250
271
|
|
|
272
|
+
#### String-based Queries for Advanced Syntax
|
|
273
|
+
|
|
274
|
+
For advanced queries like wildcard matching, use string-based queries:
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
# Raw string queries (user responsible for proper escaping)
|
|
278
|
+
variants = ProductVariant.where("sku:*") # Wildcard matching
|
|
279
|
+
customers = Customer.where("email:*@example.com AND orders_count:>5")
|
|
280
|
+
|
|
281
|
+
# String queries with parameter binding (safe, with automatic escaping)
|
|
282
|
+
# Positional parameters
|
|
283
|
+
variants = ProductVariant.where("sku:? AND product_id:?", "Test's Product", 123)
|
|
284
|
+
|
|
285
|
+
# Named parameters (as hash)
|
|
286
|
+
customers = Customer.where("email::email AND first_name::name",
|
|
287
|
+
{ email: "test@example.com", name: "John" })
|
|
288
|
+
|
|
289
|
+
# Named parameters (as keyword arguments - more convenient!)
|
|
290
|
+
variants = ProductVariant.where("sku::sku", sku: "Test's Product")
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**Query safety levels:**
|
|
294
|
+
- **Hash queries**: Fully safe, all values are automatically escaped (wildcards become literals)
|
|
295
|
+
- **String with binding**: Safe, bound parameters are automatically escaped
|
|
296
|
+
- **Raw strings**: User responsible for escaping; allows advanced syntax like wildcards
|
|
297
|
+
|
|
251
298
|
The `where` method automatically converts Ruby conditions into Shopify's GraphQL query syntax and validates that the query fields are supported by Shopify.
|
|
252
299
|
|
|
300
|
+
### Pagination
|
|
301
|
+
|
|
302
|
+
ActiveShopifyGraphQL supports cursor-based pagination for efficiently working with large result sets. Queries return a chainable `Query::Scope` that provides both automatic and manual pagination.
|
|
303
|
+
|
|
304
|
+
#### Basic Pagination with Limits
|
|
305
|
+
|
|
306
|
+
Use the `limit` method to control the total number of records fetched:
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
# Fetch up to 100 records (automatically handles pagination behind the scenes)
|
|
310
|
+
variants = ProductVariant.where(sku: "*").limit(100).to_a
|
|
311
|
+
|
|
312
|
+
# Chainable with other query methods
|
|
313
|
+
customers = Customer.where(email: "*@example.com").limit(500).to_a
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
#### Manual Pagination
|
|
317
|
+
|
|
318
|
+
Use `in_pages` without a block to manually navigate through pages:
|
|
319
|
+
|
|
320
|
+
```ruby
|
|
321
|
+
# Get first page (50 records per page)
|
|
322
|
+
page = ProductVariant.where(sku: "FRZ*").in_pages(of: 50)
|
|
323
|
+
|
|
324
|
+
page.size # => 50
|
|
325
|
+
page.has_next_page? # => true
|
|
326
|
+
page.end_cursor # => "eyJsYXN0X2lk..."
|
|
327
|
+
|
|
328
|
+
# Navigate to next page
|
|
329
|
+
next_page = page.next_page
|
|
330
|
+
next_page.size # => 50
|
|
331
|
+
|
|
332
|
+
# Navigate backwards
|
|
333
|
+
prev_page = next_page.previous_page
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
#### Automatic Pagination with Blocks
|
|
337
|
+
|
|
338
|
+
Process records in batches to control memory usage:
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
# Process 10 records at a time
|
|
342
|
+
ProductVariant.where(sku: "*").in_pages(of: 10) do |page|
|
|
343
|
+
page.each do |variant|
|
|
344
|
+
MemoryExpensiveThing.run(variant)
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Combine with limit to stop after a certain number of records
|
|
349
|
+
ProductVariant.where(sku: "*").limit(500).in_pages(of: 50) do |page|
|
|
350
|
+
# Processes 10 pages of 50 records each, then stops
|
|
351
|
+
process_batch(page.to_a)
|
|
352
|
+
end
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
#### Lazy Enumeration
|
|
356
|
+
|
|
357
|
+
The `Query::Scope` returned by `where` is enumerable and lazy-loads records:
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
# These don't execute queries immediately
|
|
361
|
+
scope = Customer.where(email: "*@example.com")
|
|
362
|
+
|
|
363
|
+
# Query executes when you iterate or convert to array
|
|
364
|
+
scope.each { |customer| puts customer.email }
|
|
365
|
+
scope.to_a # Returns array of all records
|
|
366
|
+
scope.first # Fetches just the first record
|
|
367
|
+
scope.empty? # Checks if any records exist
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**Note:** Shopify imposes a maximum of 250 records per page. The `in_pages(of: n)` method will cap `n` at 250.
|
|
371
|
+
|
|
253
372
|
### Optimizing Queries with Select
|
|
254
373
|
|
|
255
374
|
Use the `select` method to only fetch specific attributes, reducing GraphQL query size and improving performance:
|
|
@@ -277,9 +396,7 @@ ActiveShopifyGraphQL provides ActiveRecord-like associations to define relations
|
|
|
277
396
|
Use `has_many` to define one-to-many relationships:
|
|
278
397
|
|
|
279
398
|
```ruby
|
|
280
|
-
class Customer
|
|
281
|
-
include ActiveShopifyGraphQL::Base
|
|
282
|
-
|
|
399
|
+
class Customer < ActiveShopifyGraphQL::Model
|
|
283
400
|
graphql_type "Customer"
|
|
284
401
|
|
|
285
402
|
attribute :id, type: :string
|
|
@@ -314,9 +431,7 @@ customer.rewards
|
|
|
314
431
|
Use `has_one` to define one-to-one relationships:
|
|
315
432
|
|
|
316
433
|
```ruby
|
|
317
|
-
class Order
|
|
318
|
-
include ActiveShopifyGraphQL::Base
|
|
319
|
-
|
|
434
|
+
class Order < ActiveShopifyGraphQL::Model
|
|
320
435
|
has_one :billing_address, class_name: 'Address'
|
|
321
436
|
end
|
|
322
437
|
```
|
|
@@ -361,9 +476,7 @@ ActiveShopifyGraphQL supports GraphQL connections for loading related data from
|
|
|
361
476
|
Use the `connection` class method to define connections to other ActiveShopifyGraphQL models:
|
|
362
477
|
|
|
363
478
|
```ruby
|
|
364
|
-
class Customer
|
|
365
|
-
include ActiveShopifyGraphQL::Base
|
|
366
|
-
|
|
479
|
+
class Customer < ActiveShopifyGraphQL::Model
|
|
367
480
|
graphql_type 'Customer'
|
|
368
481
|
|
|
369
482
|
attribute :id
|
|
@@ -396,9 +509,7 @@ class Customer
|
|
|
396
509
|
}
|
|
397
510
|
end
|
|
398
511
|
|
|
399
|
-
class Order
|
|
400
|
-
include ActiveShopifyGraphQL::Base
|
|
401
|
-
|
|
512
|
+
class Order < ActiveShopifyGraphQL::Model
|
|
402
513
|
graphql_type 'Order'
|
|
403
514
|
|
|
404
515
|
attribute :id
|
|
@@ -414,10 +525,10 @@ fragment CustomerFragment on Customer {
|
|
|
414
525
|
id
|
|
415
526
|
displayName
|
|
416
527
|
orders(first: 2) {
|
|
417
|
-
|
|
528
|
+
nodes { id name }
|
|
418
529
|
}
|
|
419
530
|
recent_orders: orders(first: 5, reverse: true, sortKey: CREATED_AT) {
|
|
420
|
-
|
|
531
|
+
nodes { id name }
|
|
421
532
|
}
|
|
422
533
|
}
|
|
423
534
|
```
|
|
@@ -487,9 +598,7 @@ puts customer.orders.loaded? # => This won't be a proxy since data was eager
|
|
|
487
598
|
For connections that should always be loaded, you can use the `eager_load: true` parameter when defining the connection. This will automatically include the connection in all find and where queries without needing to explicitly use `includes`:
|
|
488
599
|
|
|
489
600
|
```ruby
|
|
490
|
-
class Customer
|
|
491
|
-
include ActiveShopifyGraphQL::Base
|
|
492
|
-
|
|
601
|
+
class Customer < ActiveShopifyGraphQL::Model
|
|
493
602
|
graphql_type 'Customer'
|
|
494
603
|
|
|
495
604
|
attribute :id
|
|
@@ -523,25 +632,21 @@ query customer($id: ID!) {
|
|
|
523
632
|
|
|
524
633
|
# Eager-loaded connections
|
|
525
634
|
orders(first: 10, sortKey: CREATED_AT, reverse: false) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
amount
|
|
533
|
-
}
|
|
635
|
+
nodes {
|
|
636
|
+
id
|
|
637
|
+
name
|
|
638
|
+
totalPriceSet {
|
|
639
|
+
shopMoney {
|
|
640
|
+
amount
|
|
534
641
|
}
|
|
535
642
|
}
|
|
536
643
|
}
|
|
537
644
|
}
|
|
538
645
|
addresses(first: 5, sortKey: CREATED_AT, reverse: false) {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
city
|
|
544
|
-
}
|
|
646
|
+
nodes {
|
|
647
|
+
id
|
|
648
|
+
address1
|
|
649
|
+
city
|
|
545
650
|
}
|
|
546
651
|
}
|
|
547
652
|
}
|
|
@@ -587,9 +692,7 @@ When you have bidirectional relationships between models, you can use the `inver
|
|
|
587
692
|
#### Basic Usage
|
|
588
693
|
|
|
589
694
|
```ruby
|
|
590
|
-
class Product
|
|
591
|
-
include ActiveShopifyGraphQL::Base
|
|
592
|
-
|
|
695
|
+
class Product < ActiveShopifyGraphQL::Model
|
|
593
696
|
graphql_type 'Product'
|
|
594
697
|
|
|
595
698
|
attribute :id
|
|
@@ -602,9 +705,7 @@ class Product
|
|
|
602
705
|
default_arguments: { first: 10 }
|
|
603
706
|
end
|
|
604
707
|
|
|
605
|
-
class ProductVariant
|
|
606
|
-
include ActiveShopifyGraphQL::Base
|
|
607
|
-
|
|
708
|
+
class ProductVariant < ActiveShopifyGraphQL::Model
|
|
608
709
|
graphql_type 'ProductVariant'
|
|
609
710
|
|
|
610
711
|
attribute :id
|
|
@@ -666,9 +767,10 @@ Connection queries use the same error handling as regular model queries. If a co
|
|
|
666
767
|
- [x] Support `Model.where(param: value)` proxying params to the GraphQL query attribute
|
|
667
768
|
- [x] Query optimization with `select` method
|
|
668
769
|
- [x] GraphQL connections with lazy and eager loading via `Customer.includes(:orders).find(id)`
|
|
669
|
-
- [
|
|
770
|
+
- [x] Support for paginating query results with cursors
|
|
670
771
|
- [ ] Better error handling and retry mechanisms for GraphQL API calls
|
|
671
772
|
- [ ] Caching layer for frequently accessed data
|
|
773
|
+
- [ ] Multiple `.where` chaining with possibility of using `.not`
|
|
672
774
|
|
|
673
775
|
## Development
|
|
674
776
|
|
|
@@ -3,27 +3,14 @@
|
|
|
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, :
|
|
6
|
+
attr_accessor :admin_api_client, :customer_account_client_class, :logger, :log_queries, :max_objects_per_paginated_query
|
|
7
7
|
|
|
8
8
|
def initialize
|
|
9
9
|
@admin_api_client = nil
|
|
10
10
|
@customer_account_client_class = nil
|
|
11
11
|
@logger = nil
|
|
12
12
|
@log_queries = false
|
|
13
|
-
@
|
|
13
|
+
@max_objects_per_paginated_query = 250
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
|
-
|
|
17
|
-
def self.configuration
|
|
18
|
-
@configuration ||= Configuration.new
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def self.configure
|
|
22
|
-
yield(configuration)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Reset configuration (useful for testing)
|
|
26
|
-
def self.reset_configuration!
|
|
27
|
-
@configuration = Configuration.new
|
|
28
|
-
end
|
|
29
16
|
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveShopifyGraphQL
|
|
4
|
+
module Connections
|
|
5
|
+
# Handles loading records for GraphQL connections.
|
|
6
|
+
# Refactored to use LoaderContext for cleaner parameter passing.
|
|
7
|
+
class ConnectionLoader
|
|
8
|
+
attr_reader :context
|
|
9
|
+
|
|
10
|
+
def initialize(context, loader_instance:)
|
|
11
|
+
@context = context
|
|
12
|
+
@loader_instance = loader_instance
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Load records for a connection query
|
|
16
|
+
# @param query_name [String] The connection field name (e.g., 'orders', 'addresses')
|
|
17
|
+
# @param variables [Hash] The GraphQL variables (first, sort_key, reverse, query)
|
|
18
|
+
# @param parent [Object] The parent object that owns this connection
|
|
19
|
+
# @param connection_config [Hash] The connection configuration
|
|
20
|
+
# @return [Array<Object>] Array of model instances
|
|
21
|
+
def load_records(query_name, variables, parent = nil, connection_config = nil)
|
|
22
|
+
is_nested = connection_config&.dig(:nested) || parent.respond_to?(:id)
|
|
23
|
+
|
|
24
|
+
if is_nested && parent
|
|
25
|
+
load_nested_connection(query_name, variables, parent, connection_config)
|
|
26
|
+
else
|
|
27
|
+
load_root_connection(query_name, variables, connection_config)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def load_nested_connection(query_name, variables, parent, connection_config)
|
|
34
|
+
parent_type = parent.class.graphql_type_for_loader(@context.loader_class)
|
|
35
|
+
parent_query_name = parent_type.camelize(:lower)
|
|
36
|
+
singular = connection_config&.dig(:type) == :singular
|
|
37
|
+
|
|
38
|
+
query = Query::QueryBuilder.build_connection_query(
|
|
39
|
+
@context,
|
|
40
|
+
query_name: query_name,
|
|
41
|
+
variables: variables,
|
|
42
|
+
parent_query: "#{parent_query_name}(id: $id)",
|
|
43
|
+
singular: singular
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
parent_id = extract_gid(parent)
|
|
47
|
+
response_data = @loader_instance.perform_graphql_query(query, id: parent_id)
|
|
48
|
+
|
|
49
|
+
return [] if response_data.nil?
|
|
50
|
+
|
|
51
|
+
mapper = Response::ResponseMapper.new(@context)
|
|
52
|
+
attributes = mapper.map_nested_connection_response(response_data, query_name, parent, connection_config)
|
|
53
|
+
|
|
54
|
+
# ResponseMapper now returns attributes, need to build instances
|
|
55
|
+
if singular
|
|
56
|
+
return nil if attributes.nil?
|
|
57
|
+
|
|
58
|
+
wire_inverse_of(parent, attributes, connection_config)
|
|
59
|
+
ModelBuilder.build(@context.model_class, attributes)
|
|
60
|
+
else
|
|
61
|
+
return [] if attributes.nil? || attributes.empty?
|
|
62
|
+
|
|
63
|
+
attributes.each { |attrs| wire_inverse_of(parent, attrs, connection_config) }
|
|
64
|
+
ModelBuilder.build_many(@context.model_class, attributes)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def load_root_connection(query_name, variables, connection_config)
|
|
69
|
+
singular = connection_config&.dig(:type) == :singular
|
|
70
|
+
|
|
71
|
+
query = Query::QueryBuilder.build_connection_query(
|
|
72
|
+
@context,
|
|
73
|
+
query_name: query_name,
|
|
74
|
+
variables: variables,
|
|
75
|
+
parent_query: nil,
|
|
76
|
+
singular: singular
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
response_data = @loader_instance.perform_graphql_query(query)
|
|
80
|
+
|
|
81
|
+
return [] if response_data.nil?
|
|
82
|
+
|
|
83
|
+
mapper = Response::ResponseMapper.new(@context)
|
|
84
|
+
attributes = mapper.map_connection_response(response_data, query_name, connection_config)
|
|
85
|
+
|
|
86
|
+
# ResponseMapper now returns attributes, need to build instances
|
|
87
|
+
if singular
|
|
88
|
+
return nil if attributes.nil?
|
|
89
|
+
|
|
90
|
+
ModelBuilder.build(@context.model_class, attributes)
|
|
91
|
+
else
|
|
92
|
+
return [] if attributes.nil? || attributes.empty?
|
|
93
|
+
|
|
94
|
+
ModelBuilder.build_many(@context.model_class, attributes)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Wire up inverse_of associations by adding parent to attributes cache
|
|
99
|
+
def wire_inverse_of(parent, attributes, connection_config)
|
|
100
|
+
return unless attributes.is_a?(Hash) && connection_config&.dig(:inverse_of)
|
|
101
|
+
|
|
102
|
+
inverse_name = connection_config[:inverse_of]
|
|
103
|
+
attributes[:_connection_cache] ||= {}
|
|
104
|
+
|
|
105
|
+
# Check the type of the inverse connection
|
|
106
|
+
target_class = @context.model_class
|
|
107
|
+
return unless target_class.respond_to?(:connections) && target_class.connections[inverse_name]
|
|
108
|
+
|
|
109
|
+
inverse_type = target_class.connections[inverse_name][:type]
|
|
110
|
+
attributes[:_connection_cache][inverse_name] =
|
|
111
|
+
if inverse_type == :singular
|
|
112
|
+
parent
|
|
113
|
+
else
|
|
114
|
+
# For collection inverses, wrap parent in an array
|
|
115
|
+
[parent]
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def extract_gid(parent)
|
|
120
|
+
return parent.gid if parent.respond_to?(:gid) && !parent.gid.nil?
|
|
121
|
+
|
|
122
|
+
id_value = parent.id
|
|
123
|
+
parent_type = resolve_parent_type(parent)
|
|
124
|
+
|
|
125
|
+
GidHelper.normalize_gid(id_value, parent_type)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def resolve_parent_type(parent)
|
|
129
|
+
klass = parent.class
|
|
130
|
+
|
|
131
|
+
if klass.respond_to?(:graphql_type_for_loader)
|
|
132
|
+
klass.graphql_type_for_loader(@context.loader_class)
|
|
133
|
+
elsif klass.respond_to?(:graphql_type)
|
|
134
|
+
klass.graphql_type
|
|
135
|
+
else
|
|
136
|
+
klass.name
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|