5252
5353### Basic Model Setup
5454
55- Create a model that includes ` ActiveShopifyGraphQL::Base ` :
55+ Create a model that includes ` ActiveShopifyGraphQL::Base ` and define attributes directly :
5656
5757``` ruby
5858class Customer
5959 include ActiveShopifyGraphQL ::Base
6060
61- attr_accessor :id , :name , :email , :created_at
61+ # Define the GraphQL type
62+ graphql_type " Customer"
63+
64+ # Define attributes with automatic GraphQL path inference and type coercion
65+ attribute :id , type: :string
66+ attribute :name , path: " displayName" , type: :string
67+ attribute :email , path: " defaultEmailAddress.emailAddress" , type: :string
68+ attribute :created_at , type: :datetime
6269
6370 validates :id , presence: true
6471
@@ -68,65 +75,26 @@ class Customer
6875end
6976```
7077
71- ### Creating Loaders
72-
73- Create loader classes to define how to fetch and map data from Shopify's GraphQL APIs. There are two approaches: the new ** attribute-based approach** (recommended) and the legacy ** manual fragment approach** .
78+ ### Defining Attributes
7479
75- #### Attribute-Based Approach (Recommended)
80+ Attributes are now defined directly in the model class using the ` attribute ` method. The GraphQL fragments and response mapping are automatically generated!
7681
77- Use the new ` attribute ` method to define GraphQL fields with automatic type coercion and fragment generation:
82+ #### Basic Attribute Definition
7883
7984``` ruby
80- # For Admin API
81- module ActiveShopifyGraphQL ::Loaders ::AdminApi
82- class CustomerLoader < ActiveShopifyGraphQL ::AdminApiLoader
83- graphql_type " Customer"
84-
85- # Define attributes with automatic GraphQL path inference and type coercion
86- attribute :id , type: :string
87- attribute :name , path: " displayName" , type: :string
88- attribute :email , path: " defaultEmailAddress.emailAddress" , type: :string
89- attribute :created_at , type: :datetime
90-
91- # Custom transform example
92- attribute :tags , type: :string , transform: -> (tags_array) { tags_array.join(" , " ) }
93-
94- # The fragment and map_response_to_attributes are automatically generated!
95- end
96- end
97- ```
85+ class Customer
86+ include ActiveShopifyGraphQL ::Base
9887
99- #### Manual Fragment Approach (Legacy)
88+ graphql_type " Customer "
10089
101- For more complex scenarios or when you need full control over the GraphQL fragment:
90+ # Define attributes with automatic GraphQL path inference and type coercion
91+ attribute :id , type: :string
92+ attribute :name , path: " displayName" , type: :string
93+ attribute :email , path: " defaultEmailAddress.emailAddress" , type: :string
94+ attribute :created_at , type: :datetime
10295
103- ``` ruby
104- # For Admin API
105- module ActiveShopifyGraphQL ::Loaders ::AdminApi
106- class CustomerLoader < ActiveShopifyGraphQL ::AdminApiLoader
107- graphql_type " Customer"
108-
109- fragment <<~GRAPHQL
110- id
111- displayName
112- defaultEmailAddress {
113- emailAddress
114- }
115- createdAt
116- GRAPHQL
117-
118- def map_response_to_attributes (response_data )
119- customer_data = response_data.dig(" data" , " customer" )
120- return nil unless customer_data
121-
122- {
123- id: customer_data[" id" ],
124- name: customer_data[" displayName" ],
125- email: customer_data.dig(" defaultEmailAddress" , " emailAddress" ),
126- created_at: customer_data[" createdAt" ]
127- }
128- end
129- end
96+ # Custom transform example
97+ attribute :tags , type: :string , transform: -> (tags_array) { tags_array.join(" , " ) }
13098end
13199```
132100
@@ -136,10 +104,10 @@ The `attribute` method supports several options for flexibility:
136104
137105``` ruby
138106attribute :name ,
139- path: " displayName" , # Custom GraphQL path (auto-inferred if omitted)
140- type: :string , # Type coercion (:string, :integer, :float, :boolean, :datetime)
141- null: false , # Whether the attribute can be null (default: true)
142- transform: -> (value) { value.upcase } # Custom transformation block
107+ path: " displayName" , # Custom GraphQL path (auto-inferred if omitted)
108+ type: :string , # Type coercion (:string, :integer, :float, :boolean, :datetime)
109+ null: false , # Whether the attribute can be null (default: true)
110+ transform: -> (value) { value.upcase } # Custom transformation block
143111```
144112
145113** Auto-inference:** When ` path ` is omitted, it's automatically inferred by converting snake_case to camelCase (e.g., ` display_name ` → ` displayName ` ).
@@ -148,23 +116,54 @@ attribute :name,
148116
149117** Type coercion:** Automatic conversion using ActiveModel types ensures type safety.
150118
151- #### Customer Account API Example
119+ ** Array handling:** Arrays are automatically preserved regardless of the specified type.
120+
121+ #### Metafield Attributes
152122
153- The Customer Account API has different field names and doesn't require an ID for customer queries :
123+ Shopify metafields can be easily accessed using the ` metafield_attribute ` method :
154124
155125``` ruby
156- # For Customer Account API
157- module ActiveShopifyGraphQL ::Loaders ::CustomerAccountApi
158- class CustomerLoader < ActiveShopifyGraphQL ::CustomerAccountApiLoader
159- graphql_type " Customer"
126+ class Product
127+ include ActiveShopifyGraphQL ::Base
128+
129+ graphql_type " Product"
130+
131+ # Regular attributes
132+ attribute :id , type: :string
133+ attribute :title , type: :string
134+
135+ # Metafield attributes
136+ metafield_attribute :boxes_available , namespace: ' custom' , key: ' available_boxes' , type: :integer
137+ metafield_attribute :seo_description , namespace: ' seo' , key: ' meta_description' , type: :string
138+ metafield_attribute :product_data , namespace: ' custom' , key: ' data' , type: :json
139+ metafield_attribute :is_featured , namespace: ' custom' , key: ' featured' , type: :boolean , null: false
140+ end
141+ ```
142+
143+ The metafield attributes automatically generate the correct GraphQL syntax and handle value extraction from either ` value ` or ` jsonValue ` fields based on the type.
144+
145+ #### API-Specific Attributes
146+
147+ For models that need different attributes depending on the API being used, you can define loader-specific overrides:
148+
149+ ``` ruby
150+ class Customer
151+ include ActiveShopifyGraphQL ::Base
152+
153+ graphql_type " Customer"
160154
161- # Customer Account API uses different field names
162- attribute :id , type: :string
155+ # Default attributes (used by Admin API)
156+ attribute :id , type: :string
157+ attribute :name , path: " displayName" , type: :string
158+ attribute :email , path: " defaultEmailAddress.emailAddress" , type: :string
159+ attribute :created_at , type: :datetime
160+
161+ # Customer Account API uses different field names
162+ for_loader ActiveShopifyGraphQL ::CustomerAccountApiLoader do
163163 attribute :name , path: " firstName" , type: :string
164164 attribute :last_name , path: " lastName" , type: :string
165165 attribute :email , path: " emailAddress.emailAddress" , type: :string
166166 attribute :phone , path: " phoneNumber.phoneNumber" , type: :string , null: true
167- attribute :created_at , path: " createdAt" , type: :datetime
168167 end
169168end
170169```
@@ -174,11 +173,10 @@ end
174173Use the ` find ` method to retrieve records by ID:
175174
176175``` ruby
177- # Using default loader ( Admin API)
176+ # Using Admin API (default )
178177customer = Customer .find(" gid://shopify/Customer/123456789" )
179-
180- # Using specific loader
181- customer = Customer .find(" gid://shopify/Customer/123456789" , loader: custom_loader)
178+ # You can also use just the ID number
179+ customer = Customer .find(123456789 )
182180
183181# Using Customer Account API
184182customer = Customer .with_customer_account_api(token).find
@@ -206,7 +204,6 @@ Use the `where` method to query multiple records using Shopify's search syntax:
206204``` ruby
207205# Simple conditions
208206customers = Customer .where(email: " john@example.com" )
209- customers = Customer .where(first_name: " John" , country: " Canada" )
210207
211208# Range queries
212209customers = Customer .where(created_at: { gte: " 2024-01-01" , lt: " 2024-02-01" })
@@ -215,12 +212,48 @@ customers = Customer.where(orders_count: { gte: 5 })
215212# Multi-word values are automatically quoted
216213customers = Customer .where(first_name: " John Doe" )
217214
218- # With custom loader and limits
219- customers = Customer .where({ email: " john@example.com" }, loader: custom_loader, limit: 100 )
215+ # With limits
216+ customers = Customer .where({ email: " john@example.com" }, limit: 100 )
220217```
221218
222219The ` where ` method automatically converts Ruby conditions into Shopify's GraphQL query syntax and validates that the query fields are supported by Shopify.
223220
221+ ### Optimizing Queries with Select
222+
223+ Use the ` select ` method to only fetch specific attributes, reducing GraphQL query size and improving performance:
224+
225+ ``` ruby
226+ # Only fetch id, name, and email
227+ customer = Customer .select (:id , :name , :email ).find(123 )
228+
229+ # Works with where queries too
230+ customers = Customer .select (:id , :name ).where(country: " Canada" )
231+
232+ # Always includes id even if not specified
233+ customer = Customer .select (:name ).find(123 )
234+ # This will still include :id in the GraphQL query
235+ ```
236+
237+ The ` select ` method validates that the specified attributes exist and automatically includes the ` id ` field for proper object identification.
238+
239+ ### Optimizing Queries with Select
240+
241+ Use the ` select ` method to only fetch specific attributes, reducing GraphQL query size and improving performance:
242+
243+ ``` ruby
244+ # Only fetch id, name, and email
245+ customer = Customer .select (:id , :name , :email ).find(123 )
246+
247+ # Works with where queries too
248+ customers = Customer .select (:id , :name ).where(country: " Canada" )
249+
250+ # Always includes id even if not specified
251+ customer = Customer .select (:name ).find(123 )
252+ # This will still include :id in the GraphQL query
253+ ```
254+
255+ The ` select ` method validates that the specified attributes exist and automatically includes the ` id ` field for proper object identification.
256+
224257## Associations
225258
226259ActiveShopifyGraphQL provides ActiveRecord-like associations to define relationships between the Shopify native models and your own custom ones.
@@ -233,22 +266,19 @@ Use `has_many` to define one-to-many relationships:
233266class Customer
234267 include ActiveShopifyGraphQL ::Base
235268
236- attr_accessor :id , :display_name , :email , :created_at
269+ graphql_type " Customer"
270+
271+ attribute :id , type: :string
272+ attribute :display_name , type: :string
273+ attribute :email , path: " defaultEmailAddress.emailAddress" , type: :string
274+ attribute :created_at , type: :datetime
237275
238276 # Define an association to one of your own ActiveRecord models
239277 # foreign_key maps the id of the GraphQL powered model to the rewards.shopify_customer_id table
240278 has_many :rewards , foreign_key: :shopify_customer_id
241279
242280 validates :id , presence: true
243281end
244-
245- class Order
246- include ActiveShopifyGraphQL ::Base
247-
248- attr_accessor :id , :name , :shopify_customer_id , :created_at
249-
250- validates :id , presence: true
251- end
252282```
253283
254284#### Using the Association
@@ -279,6 +309,9 @@ The associations automatically handle Shopify GID format conversion, extracting
279309## Next steps
280310
281311- [x] Support ` Model.where(param: value) ` proxying params to the GraphQL query attribute
312+ - [x] Attribute-based model definition with automatic GraphQL fragment generation
313+ - [x] Metafield attributes for easy access to Shopify metafields
314+ - [x] Query optimization with ` select ` method
282315- [ ] Eager loading of GraphQL connections via ` Customer.includes(:orders).find(id) ` in a single GraphQL query
283316- [ ] Better error handling and retry mechanisms for GraphQL API calls
284317- [ ] Caching layer for frequently accessed data
0 commit comments