shopify_toolkit 0.5.0 → 0.5.2
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/README.md +44 -3
- data/lib/shopify_toolkit/command_line.rb +28 -0
- data/lib/shopify_toolkit/metafield_statements.rb +21 -0
- data/lib/shopify_toolkit/metaobject_statements.rb +142 -24
- data/lib/shopify_toolkit/metaobject_utilities.rb +107 -0
- data/lib/shopify_toolkit/migrator.rb +1 -0
- data/lib/shopify_toolkit/schema.rb +223 -11
- data/lib/shopify_toolkit/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 767c783d8c115219c9b25c83e6defa01772cc321a411c072e149ed82e10bc2ba
|
|
4
|
+
data.tar.gz: 576dfd3753579fac15388f5b22186a5b35b9298d974875ace256532b0af2063d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e441dfcdffb2552f0773d132db88a8204a0f60c2624f40f8cff2b0d01304de480e92a95d8d6db5f396bec0c1bce7179d1315fb49b3bb0347f59c14070d4b1685
|
|
7
|
+
data.tar.gz: 14786f1aa5d5d4e27274f591996f90960c6fdd326f1613088fb54f52f26ebf447b06eeaea4ebf3455c0f868bd9d5f1e71e176c58b75d15c499dd0dd9186caf2f
|
data/README.md
CHANGED
|
@@ -24,6 +24,10 @@ A toolkit for working with Custom Shopify Apps built on Rails.
|
|
|
24
24
|
- [ ] GraphQL Admin API client with built-in caching
|
|
25
25
|
- [ ] GraphQL Admin API client with built-in error handling
|
|
26
26
|
- [ ] GraphQL Admin API client with built-in logging
|
|
27
|
+
- [ ] Bulk Operations
|
|
28
|
+
- [ ] Interface for uploading and getting results for query / mutation
|
|
29
|
+
- [ ] Error handling and Logging
|
|
30
|
+
- [ ] Callbacks
|
|
27
31
|
|
|
28
32
|
## Installation
|
|
29
33
|
|
|
@@ -40,10 +44,11 @@ bundle add shopify_toolkit
|
|
|
40
44
|
Within a Rails application created with ShopifyApp, generate a new migration file:
|
|
41
45
|
|
|
42
46
|
```bash
|
|
43
|
-
|
|
47
|
+
bundle exec shopify-toolkit generate_migration AddProductPressReleases
|
|
44
48
|
```
|
|
45
49
|
|
|
46
50
|
Then, add the following code to the migration file:
|
|
51
|
+
|
|
47
52
|
```ruby
|
|
48
53
|
# config/shopify/migrate/20250528130134_add_product_press_releases.rb
|
|
49
54
|
class AddProductPressReleases < ShopifyToolkit::Migration
|
|
@@ -96,6 +101,40 @@ ShopifyToolkit::MetafieldSchema.define do
|
|
|
96
101
|
end
|
|
97
102
|
```
|
|
98
103
|
|
|
104
|
+
### Monkey Patching for Specific Use Cases
|
|
105
|
+
|
|
106
|
+
In some scenarios, you may need to customize the behavior of ShopifyToolkit to handle specific limitations or requirements. For example, you might want to skip certain Shopify-managed metaobject definitions that cannot be created due to reserved namespaces or limited access permissions.
|
|
107
|
+
|
|
108
|
+
```rb
|
|
109
|
+
# config/initializers/shopify_toolkit_patches.rb
|
|
110
|
+
|
|
111
|
+
require "shopify_toolkit/metaobject_statements"
|
|
112
|
+
|
|
113
|
+
module ShopifyToolkit
|
|
114
|
+
module MetaobjectStatements
|
|
115
|
+
# Override create_metaobject_definition to skip Shopify-managed definitions
|
|
116
|
+
alias_method :original_create_metaobject_definition, :create_metaobject_definition
|
|
117
|
+
|
|
118
|
+
def create_metaobject_definition(type, **options)
|
|
119
|
+
# Skip Shopify-managed metaobject definitions during schema load
|
|
120
|
+
if type.to_s.start_with?("shopify--")
|
|
121
|
+
say "Skipping Shopify-managed metaobject definition: #{type}"
|
|
122
|
+
return
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
original_create_metaobject_definition(type, **options)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
This approach allows you to:
|
|
132
|
+
|
|
133
|
+
- Skip creation of reserved metaobject types (e.g., those prefixed with `shopify--`)
|
|
134
|
+
- Handle cases where certain metaobjects cannot be created due to API limitations
|
|
135
|
+
- Customize validation logic for specific metaobject types
|
|
136
|
+
- Filter out problematic metafield references during schema loading
|
|
137
|
+
|
|
99
138
|
### Analyzing a Matrixify CSV Result files
|
|
100
139
|
|
|
101
140
|
Matrixify is a popular Shopify app that allows you to import/export data from Shopify using CSV files. The CSV files that Matrixify generates are very verbose and can be difficult to work with. This tool allows you to analyze the CSV files and extract the data you need.
|
|
@@ -104,8 +143,10 @@ The tool will import the file into a local SQLite database
|
|
|
104
143
|
and open a console for you to run queries against the data
|
|
105
144
|
using ActiveRecord.
|
|
106
145
|
|
|
146
|
+
> Note: To use it with `bundle exec`, sqlite3 must be included in the project’s bundle
|
|
147
|
+
|
|
107
148
|
```shell
|
|
108
|
-
shopify-
|
|
149
|
+
shopify-toolkit analyze products-result.csv --force-import
|
|
109
150
|
==> Importing products-result.csv into /var/folders/hn/z7b7s1kj3js4k7_qk3lj27kr0000gn/T/shopify-toolkit-analyze-products_result_csv.sqlite3
|
|
110
151
|
-- create_table(:results, {:force=>true})
|
|
111
152
|
-> 0.0181s
|
|
@@ -119,7 +160,7 @@ shopify-csv analyze products-result.csv --force-import
|
|
|
119
160
|
"UPDATE: Found by Handle | Assuming MERGE for Variant Command | Assuming MERGE for Tags Command | Variants updated by SKU: 1 | Warning: The following media were not uploaded to Shopify: [https://mymedia.com/image.jpg: Error downloading from Web: 302 Moved Temporarily]",
|
|
120
161
|
...]
|
|
121
162
|
>> first
|
|
122
|
-
#<
|
|
163
|
+
#<ShopifyToolkit::Result:0x0000000300bf1668
|
|
123
164
|
id: 1,
|
|
124
165
|
data: nil,
|
|
125
166
|
handle: "my-product",
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
require 'csv'
|
|
2
2
|
require 'active_record'
|
|
3
3
|
require 'thor'
|
|
4
|
+
require 'thor/actions'
|
|
4
5
|
require 'tmpdir'
|
|
5
6
|
|
|
6
7
|
class ShopifyToolkit::CommandLine < Thor
|
|
8
|
+
include Thor::Actions
|
|
9
|
+
|
|
7
10
|
RESERVED_COLUMN_NAMES = %w[select type id]
|
|
8
11
|
|
|
9
12
|
class Result < ActiveRecord::Base
|
|
@@ -83,6 +86,31 @@ class ShopifyToolkit::CommandLine < Thor
|
|
|
83
86
|
::Shop.sole.with_shopify_session { ShopifyToolkit::Schema.dump! }
|
|
84
87
|
end
|
|
85
88
|
|
|
89
|
+
desc "generate_migration NAME", "Generate a migration with the given NAME"
|
|
90
|
+
def generate_migration(name)
|
|
91
|
+
require "./config/environment"
|
|
92
|
+
migrations_dir = Rails.root.join("config/shopify/migrate")
|
|
93
|
+
file_name = "#{Time.now.utc.strftime('%Y%m%d%H%M%S')}_#{name.underscore}.rb"
|
|
94
|
+
|
|
95
|
+
if migrations_dir.entries.map(&:to_s).grep(/\A\d+_#{Regexp.escape name.underscore}\.rb\z/).any?
|
|
96
|
+
raise Thor::Error, "Migration class already exists: #{file_name}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
create_file migrations_dir.join(file_name) do
|
|
100
|
+
<<~RUBY
|
|
101
|
+
class #{name.camelize} < ShopifyToolkit::Migration
|
|
102
|
+
def up
|
|
103
|
+
# Add your migration code here
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def down
|
|
107
|
+
# Add your rollback code here
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
RUBY
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
86
114
|
def self.exit_on_failure?
|
|
87
115
|
true
|
|
88
116
|
end
|
|
@@ -6,6 +6,7 @@ module ShopifyToolkit::MetafieldStatements
|
|
|
6
6
|
extend ActiveSupport::Concern
|
|
7
7
|
include ShopifyToolkit::Migration::Logging
|
|
8
8
|
include ShopifyToolkit::AdminClient
|
|
9
|
+
include ShopifyToolkit::MetaobjectUtilities
|
|
9
10
|
|
|
10
11
|
def self.log_time(method_name)
|
|
11
12
|
current_method = instance_method(method_name)
|
|
@@ -26,6 +27,21 @@ module ShopifyToolkit::MetafieldStatements
|
|
|
26
27
|
return
|
|
27
28
|
end
|
|
28
29
|
|
|
30
|
+
# Process validations to convert metaobject types to GIDs (only for metaobject reference fields)
|
|
31
|
+
if options[:validations] && is_metaobject_reference_type?(type)
|
|
32
|
+
begin
|
|
33
|
+
options[:validations] = convert_validations_types_to_gids(options[:validations])
|
|
34
|
+
rescue RuntimeError => e
|
|
35
|
+
if e.message.include?("not found")
|
|
36
|
+
say "ERROR: Cannot create metafield #{namespace}:#{key} - references non-existent metaobject. This suggests the metaobject was filtered out or failed to create."
|
|
37
|
+
say " Original error: #{e.message}"
|
|
38
|
+
return
|
|
39
|
+
else
|
|
40
|
+
raise e
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
29
45
|
# https://shopify.dev/docs/api/admin-graphql/2024-10/mutations/metafieldDefinitionCreate
|
|
30
46
|
query =
|
|
31
47
|
"# GraphQL
|
|
@@ -117,6 +133,11 @@ module ShopifyToolkit::MetafieldStatements
|
|
|
117
133
|
return
|
|
118
134
|
end
|
|
119
135
|
|
|
136
|
+
# Process validations to convert metaobject types to GIDs (only for metaobject reference fields)
|
|
137
|
+
if options[:validations] && options[:type] && is_metaobject_reference_type?(options[:type])
|
|
138
|
+
options[:validations] = convert_validations_types_to_gids(options[:validations])
|
|
139
|
+
end
|
|
140
|
+
|
|
120
141
|
shopify_admin_client
|
|
121
142
|
.query(
|
|
122
143
|
# Documentation: https://shopify.dev/docs/api/admin-graphql/2024-10/mutations/metafieldDefinitionUpdate
|
|
@@ -6,6 +6,9 @@ module ShopifyToolkit::MetaobjectStatements
|
|
|
6
6
|
extend ActiveSupport::Concern
|
|
7
7
|
include ShopifyToolkit::Migration::Logging
|
|
8
8
|
include ShopifyToolkit::AdminClient
|
|
9
|
+
include ShopifyToolkit::MetaobjectUtilities
|
|
10
|
+
|
|
11
|
+
@@pending_field_validations = []
|
|
9
12
|
|
|
10
13
|
def self.log_time(method_name)
|
|
11
14
|
current_method = instance_method(method_name)
|
|
@@ -14,18 +17,152 @@ module ShopifyToolkit::MetaobjectStatements
|
|
|
14
17
|
end
|
|
15
18
|
end
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
def apply_pending_field_validations
|
|
21
|
+
return if @@pending_field_validations.empty?
|
|
22
|
+
|
|
23
|
+
say "Applying #{@@pending_field_validations.size} pending field validations"
|
|
24
|
+
|
|
25
|
+
@@pending_field_validations.reject! do |item|
|
|
26
|
+
metaobject_type = item[:metaobject_type]
|
|
27
|
+
field_key = item[:field_key]
|
|
28
|
+
validations = item[:validations]
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
success = add_field_validations_to_metaobject(metaobject_type, field_key, validations, item)
|
|
32
|
+
unless success
|
|
33
|
+
say "-- Deferring field '#{field_key}' in '#{metaobject_type}' (missing dependencies)"
|
|
34
|
+
end
|
|
35
|
+
success
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
say "-- Failed to process field '#{field_key}' in '#{metaobject_type}': #{e.message}"
|
|
38
|
+
true # Remove from array (don't retry errors)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
19
43
|
log_time \
|
|
20
44
|
def create_metaobject_definition(type, **options)
|
|
21
|
-
# Skip creation if
|
|
45
|
+
# Skip creation if metaobject already exists
|
|
22
46
|
existing_gid = get_metaobject_definition_gid(type)
|
|
23
47
|
if existing_gid
|
|
24
48
|
say "Metaobject #{type} already exists, skipping creation"
|
|
25
49
|
return existing_gid
|
|
26
50
|
end
|
|
27
51
|
|
|
28
|
-
#
|
|
52
|
+
# Transform options for GraphQL API
|
|
53
|
+
definition = options.merge(type: type.to_s)
|
|
54
|
+
|
|
55
|
+
begin
|
|
56
|
+
# Convert field_definitions to fieldDefinitions and transform field structure
|
|
57
|
+
if options[:field_definitions]
|
|
58
|
+
definition[:fieldDefinitions] = options[:field_definitions].filter_map do |field|
|
|
59
|
+
field_def = build_field_definition(field)
|
|
60
|
+
field_needs_validations = is_metaobject_reference_type?(field[:type])
|
|
61
|
+
|
|
62
|
+
if field[:validations]
|
|
63
|
+
begin
|
|
64
|
+
converted_validations = convert_validations_types_to_gids(field[:validations])
|
|
65
|
+
field_def[:validations] = converted_validations if converted_validations&.any?
|
|
66
|
+
rescue RuntimeError => e
|
|
67
|
+
if e.message.include?("not found")
|
|
68
|
+
@@pending_field_validations << {
|
|
69
|
+
metaobject_type: type,
|
|
70
|
+
field_key: field[:key],
|
|
71
|
+
field_definition: field,
|
|
72
|
+
validations: field[:validations]
|
|
73
|
+
}
|
|
74
|
+
say "Deferring field '#{field[:key]}' in '#{type}' (missing dependency)"
|
|
75
|
+
|
|
76
|
+
if field_needs_validations
|
|
77
|
+
next # Skip this field entirely
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
elsif field_needs_validations
|
|
82
|
+
next # Skip fields that need validations but don't have any
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
field_def
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
definition.delete(:field_definitions)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Remove admin access to avoid API restrictions
|
|
92
|
+
if definition[:access]&.is_a?(Hash)
|
|
93
|
+
definition[:access] = definition[:access].dup
|
|
94
|
+
definition[:access].delete("admin") if definition[:access]["admin"]
|
|
95
|
+
definition.delete(:access) if definition[:access].empty?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Clean up empty validations arrays that cause API errors
|
|
99
|
+
if definition[:fieldDefinitions]
|
|
100
|
+
definition[:fieldDefinitions].each do |field_def|
|
|
101
|
+
field_def.delete(:validations) if field_def[:validations]&.empty?
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
result = create_metaobject_definition_immediate(definition)
|
|
106
|
+
result
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def add_field_validations_to_metaobject(metaobject_type, field_key, validations, item = nil)
|
|
111
|
+
# Get the existing metaobject definition
|
|
112
|
+
existing_gid = get_metaobject_definition_gid(metaobject_type)
|
|
113
|
+
unless existing_gid
|
|
114
|
+
say "Error: Cannot add validations to '#{metaobject_type}' - metaobject not found"
|
|
115
|
+
return false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
begin
|
|
119
|
+
converted_validations = convert_validations_types_to_gids(validations)
|
|
120
|
+
|
|
121
|
+
if converted_validations&.any?
|
|
122
|
+
# Use the passed item, or try to find it (for backward compatibility)
|
|
123
|
+
if item.nil?
|
|
124
|
+
item = @@pending_field_validations.find { |pending_item|
|
|
125
|
+
pending_item[:metaobject_type] == metaobject_type && pending_item[:field_key] == field_key
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
if item && item[:field_definition]
|
|
130
|
+
field_def = item[:field_definition]
|
|
131
|
+
new_field = build_field_definition(field_def, converted_validations)
|
|
132
|
+
|
|
133
|
+
field_operation = { create: new_field }
|
|
134
|
+
update_metaobject_definition(metaobject_type, fieldDefinitions: [field_operation])
|
|
135
|
+
|
|
136
|
+
say "Added field '#{field_key}' to '#{metaobject_type}'"
|
|
137
|
+
return true
|
|
138
|
+
else
|
|
139
|
+
return false
|
|
140
|
+
end
|
|
141
|
+
else
|
|
142
|
+
return false
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
rescue RuntimeError
|
|
146
|
+
return false # Keep trying later or don't retry errors
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def build_field_definition(field, validations = nil)
|
|
153
|
+
field_def = {
|
|
154
|
+
key: field[:key].to_s,
|
|
155
|
+
name: field[:name],
|
|
156
|
+
type: field[:type].to_s
|
|
157
|
+
}
|
|
158
|
+
field_def[:description] = field[:description] if field[:description]
|
|
159
|
+
field_def[:required] = field[:required] if field[:required]
|
|
160
|
+
field_def[:validations] = validations if validations&.any?
|
|
161
|
+
field_def
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def create_metaobject_definition_immediate(definition)
|
|
165
|
+
# https://shopify.dev/docs/api/admin-graphql/2024-10/mutations/metaobjectDefinitionCreate
|
|
29
166
|
query =
|
|
30
167
|
"# GraphQL
|
|
31
168
|
mutation CreateMetaobjectDefinition($definition: MetaobjectDefinitionCreateInput!) {
|
|
@@ -47,32 +184,13 @@ module ShopifyToolkit::MetaobjectStatements
|
|
|
47
184
|
}
|
|
48
185
|
}
|
|
49
186
|
"
|
|
50
|
-
variables = { definition:
|
|
187
|
+
variables = { definition: definition }
|
|
51
188
|
|
|
52
189
|
shopify_admin_client
|
|
53
190
|
.query(query:, variables:)
|
|
54
191
|
.tap { handle_shopify_admin_client_errors(_1, "data.metaobjectDefinitionCreate.userErrors") }
|
|
55
192
|
end
|
|
56
193
|
|
|
57
|
-
def get_metaobject_definition_gid(type)
|
|
58
|
-
result =
|
|
59
|
-
shopify_admin_client
|
|
60
|
-
.query(
|
|
61
|
-
query:
|
|
62
|
-
"# GraphQL
|
|
63
|
-
query GetMetaobjectDefinitionID($type: String!) {
|
|
64
|
-
metaobjectDefinitionByType(type: $type) {
|
|
65
|
-
id
|
|
66
|
-
}
|
|
67
|
-
}",
|
|
68
|
-
variables: { type: type.to_s },
|
|
69
|
-
)
|
|
70
|
-
.tap { handle_shopify_admin_client_errors(_1) }
|
|
71
|
-
.body
|
|
72
|
-
|
|
73
|
-
result.dig("data", "metaobjectDefinitionByType", "id")
|
|
74
|
-
end
|
|
75
|
-
|
|
76
194
|
def update_metaobject_definition(type, **options)
|
|
77
195
|
existing_gid = get_metaobject_definition_gid(type)
|
|
78
196
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
|
|
5
|
+
module ShopifyToolkit::MetaobjectUtilities
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
include ShopifyToolkit::AdminClient
|
|
8
|
+
|
|
9
|
+
def is_metaobject_reference_type?(type)
|
|
10
|
+
type.to_sym.in?([:metaobject_reference, :"list.metaobject_reference", :mixed_reference, :"list.mixed_reference"])
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def convert_validations_types_to_gids(validations)
|
|
14
|
+
return validations unless validations&.any?
|
|
15
|
+
|
|
16
|
+
validations.filter_map do |validation|
|
|
17
|
+
convert_metaobject_validation(validation)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def get_metaobject_definition_gid(type)
|
|
22
|
+
result =
|
|
23
|
+
shopify_admin_client
|
|
24
|
+
.query(
|
|
25
|
+
query:
|
|
26
|
+
"# GraphQL
|
|
27
|
+
query GetMetaobjectDefinitionID($type: String!) {
|
|
28
|
+
metaobjectDefinitionByType(type: $type) {
|
|
29
|
+
id
|
|
30
|
+
}
|
|
31
|
+
}",
|
|
32
|
+
variables: { type: type.to_s },
|
|
33
|
+
)
|
|
34
|
+
.tap { handle_shopify_admin_client_errors(_1) }
|
|
35
|
+
.body
|
|
36
|
+
|
|
37
|
+
gid = result.dig("data", "metaobjectDefinitionByType", "id")
|
|
38
|
+
|
|
39
|
+
return gid
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def get_metaobject_definition_type_by_gid(gid)
|
|
43
|
+
result =
|
|
44
|
+
shopify_admin_client
|
|
45
|
+
.query(
|
|
46
|
+
query:
|
|
47
|
+
"# GraphQL
|
|
48
|
+
query GetMetaobjectDefinitionType($id: ID!) {
|
|
49
|
+
metaobjectDefinition(id: $id) {
|
|
50
|
+
type
|
|
51
|
+
}
|
|
52
|
+
}",
|
|
53
|
+
variables: { id: gid },
|
|
54
|
+
)
|
|
55
|
+
.tap { handle_shopify_admin_client_errors(_1) }
|
|
56
|
+
.body
|
|
57
|
+
|
|
58
|
+
result.dig("data", "metaobjectDefinition", "type")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def convert_metaobject_validation(validation)
|
|
64
|
+
name = validation[:name] || validation["name"]
|
|
65
|
+
value = validation[:value] || validation["value"]
|
|
66
|
+
|
|
67
|
+
return validation unless metaobject_type_validation?(name) && value
|
|
68
|
+
|
|
69
|
+
parsed_value = parse_json_if_needed(value)
|
|
70
|
+
convert_types_to_gids(parsed_value)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def metaobject_type_validation?(name)
|
|
74
|
+
name.in?(["metaobject_definition_type", "metaobject_definition_types"])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def parse_json_if_needed(value)
|
|
78
|
+
return value unless value.is_a?(String) && value.start_with?("[") && value.end_with?("]")
|
|
79
|
+
|
|
80
|
+
JSON.parse(value)
|
|
81
|
+
rescue JSON::ParserError
|
|
82
|
+
value
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def convert_types_to_gids(value)
|
|
86
|
+
if value.is_a?(Array)
|
|
87
|
+
convert_array_types_to_gids(value)
|
|
88
|
+
else
|
|
89
|
+
convert_single_type_to_gid(value)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def convert_array_types_to_gids(types)
|
|
94
|
+
gids = types.map do |type|
|
|
95
|
+
gid = get_metaobject_definition_gid(type)
|
|
96
|
+
raise "Metaobject type '#{type}' not found" unless gid
|
|
97
|
+
gid
|
|
98
|
+
end
|
|
99
|
+
{ name: "metaobject_definition_ids", value: gids.to_json }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def convert_single_type_to_gid(type)
|
|
103
|
+
gid = get_metaobject_definition_gid(type)
|
|
104
|
+
raise "Metaobject type '#{type}' not found" unless gid
|
|
105
|
+
{ name: "metaobject_definition_id", value: gid }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -6,6 +6,7 @@ require "active_support/core_ext/module/delegation"
|
|
|
6
6
|
class ShopifyToolkit::Migrator # :nodoc:
|
|
7
7
|
include ShopifyToolkit::AdminClient
|
|
8
8
|
include ShopifyToolkit::MetafieldStatements
|
|
9
|
+
include ShopifyToolkit::MetaobjectStatements
|
|
9
10
|
|
|
10
11
|
singleton_class.attr_accessor :migrations_paths
|
|
11
12
|
self.migrations_paths = ["config/shopify/migrate"]
|
|
@@ -8,6 +8,8 @@ require "active_support/core_ext/module/delegation"
|
|
|
8
8
|
module ShopifyToolkit::Schema
|
|
9
9
|
extend self
|
|
10
10
|
include ShopifyToolkit::MetafieldStatements
|
|
11
|
+
include ShopifyToolkit::MetaobjectStatements
|
|
12
|
+
include ShopifyToolkit::MetaobjectUtilities
|
|
11
13
|
include ShopifyToolkit::Migration::Logging
|
|
12
14
|
|
|
13
15
|
delegate :logger, to: Rails
|
|
@@ -50,8 +52,19 @@ module ShopifyToolkit::Schema
|
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
announce "Loading metafield schema from #{path}"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
|
|
56
|
+
# Parse the schema file to separate metaobject and metafield definitions
|
|
57
|
+
schema_content = File.read(path)
|
|
58
|
+
|
|
59
|
+
say_with_time "Executing metaobject definitions" do
|
|
60
|
+
# Execute only metaobject definitions first
|
|
61
|
+
execute_metaobject_definitions(schema_content)
|
|
62
|
+
apply_pending_field_validations
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
say_with_time "Executing metafield definitions" do
|
|
66
|
+
# Execute only metafield definitions after all metaobjects exist
|
|
67
|
+
execute_metafield_definitions(schema_content)
|
|
55
68
|
end
|
|
56
69
|
end
|
|
57
70
|
|
|
@@ -69,6 +82,66 @@ module ShopifyToolkit::Schema
|
|
|
69
82
|
instance_eval(&block)
|
|
70
83
|
end
|
|
71
84
|
|
|
85
|
+
def convert_validations_gids_to_types(validations, metafield_type)
|
|
86
|
+
return validations unless validations&.any? && is_metaobject_reference_type?(metafield_type)
|
|
87
|
+
|
|
88
|
+
validations.filter_map do |validation|
|
|
89
|
+
convert_metaobject_validation_to_type(validation)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def convert_metaobject_validation_to_type(validation)
|
|
96
|
+
name = validation["name"]
|
|
97
|
+
value = validation["value"]
|
|
98
|
+
|
|
99
|
+
return validation unless metaobject_gid_validation?(name)
|
|
100
|
+
|
|
101
|
+
parsed_value = parse_json_if_needed(value)
|
|
102
|
+
convert_gids_to_types(validation, parsed_value)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def metaobject_gid_validation?(name)
|
|
106
|
+
name.in?(["metaobject_definition_id", "metaobject_definition_ids"])
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def convert_gids_to_types(validation, value)
|
|
110
|
+
if value.is_a?(Array)
|
|
111
|
+
convert_array_gids_to_types(validation, value)
|
|
112
|
+
else
|
|
113
|
+
convert_single_gid_to_type(validation, value)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def convert_array_gids_to_types(validation, gids)
|
|
118
|
+
types = gids.filter_map { |gid| convert_gid_to_type_if_valid(gid) }
|
|
119
|
+
|
|
120
|
+
return nil unless types.any?
|
|
121
|
+
|
|
122
|
+
validation.merge("name" => "metaobject_definition_types", "value" => types)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def convert_single_gid_to_type(validation, gid)
|
|
126
|
+
type = convert_gid_to_type_if_valid(gid)
|
|
127
|
+
|
|
128
|
+
return nil unless type
|
|
129
|
+
|
|
130
|
+
validation.merge("name" => "metaobject_definition_type", "value" => type)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def convert_gid_to_type_if_valid(gid)
|
|
134
|
+
return gid unless gid&.start_with?("gid://shopify/MetaobjectDefinition/")
|
|
135
|
+
|
|
136
|
+
type = get_metaobject_definition_type_by_gid(gid)
|
|
137
|
+
|
|
138
|
+
if type.nil?
|
|
139
|
+
say "Warning: Metafield validation references unknown metaobject GID #{gid} - excluding from portable schema"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
type
|
|
143
|
+
end
|
|
144
|
+
|
|
72
145
|
def fetch_definitions(owner_type:)
|
|
73
146
|
owner_type = owner_type.to_s.singularize.upcase
|
|
74
147
|
|
|
@@ -116,15 +189,64 @@ module ShopifyToolkit::Schema
|
|
|
116
189
|
result.dig("data", "metafieldDefinitions", "nodes") || []
|
|
117
190
|
end
|
|
118
191
|
|
|
192
|
+
def fetch_metaobject_definitions
|
|
193
|
+
query = <<~GRAPHQL
|
|
194
|
+
query {
|
|
195
|
+
metaobjectDefinitions(first: 250) {
|
|
196
|
+
nodes {
|
|
197
|
+
id
|
|
198
|
+
type
|
|
199
|
+
name
|
|
200
|
+
description
|
|
201
|
+
fieldDefinitions {
|
|
202
|
+
key
|
|
203
|
+
name
|
|
204
|
+
description
|
|
205
|
+
type {
|
|
206
|
+
name
|
|
207
|
+
}
|
|
208
|
+
required
|
|
209
|
+
validations {
|
|
210
|
+
name
|
|
211
|
+
value
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
access {
|
|
215
|
+
admin
|
|
216
|
+
storefront
|
|
217
|
+
}
|
|
218
|
+
capabilities {
|
|
219
|
+
publishable {
|
|
220
|
+
enabled
|
|
221
|
+
}
|
|
222
|
+
translatable {
|
|
223
|
+
enabled
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
GRAPHQL
|
|
230
|
+
|
|
231
|
+
result =
|
|
232
|
+
shopify_admin_client
|
|
233
|
+
.query(query:)
|
|
234
|
+
.tap { handle_shopify_admin_client_errors(_1) }
|
|
235
|
+
.body
|
|
236
|
+
|
|
237
|
+
result.dig("data", "metaobjectDefinitions", "nodes") || []
|
|
238
|
+
end
|
|
239
|
+
|
|
119
240
|
def generate_schema_content
|
|
120
|
-
|
|
241
|
+
metaobject_definitions = fetch_metaobject_definitions
|
|
242
|
+
metafield_definitions =
|
|
121
243
|
OWNER_TYPES.flat_map { |owner_type| fetch_definitions(owner_type:) }
|
|
122
244
|
|
|
123
245
|
content = StringIO.new
|
|
124
246
|
content << <<~RUBY
|
|
125
|
-
# This file is auto-generated from the current state of the Shopify metafields.
|
|
126
|
-
# Instead of editing this file, please use the
|
|
127
|
-
# to incrementally modify your metafields, and then regenerate this schema definition.
|
|
247
|
+
# This file is auto-generated from the current state of the Shopify metafields and metaobjects.
|
|
248
|
+
# Instead of editing this file, please use the migration features of ShopifyToolkit
|
|
249
|
+
# to incrementally modify your metafields and metaobjects, and then regenerate this schema definition.
|
|
128
250
|
#
|
|
129
251
|
# This file is the source used to define your metafields when running `bin/rails shopify:schema:load`.
|
|
130
252
|
#
|
|
@@ -132,8 +254,59 @@ module ShopifyToolkit::Schema
|
|
|
132
254
|
ShopifyToolkit::Schema.define do
|
|
133
255
|
RUBY
|
|
134
256
|
|
|
135
|
-
#
|
|
136
|
-
|
|
257
|
+
# Add metaobject definitions first
|
|
258
|
+
metaobject_definitions
|
|
259
|
+
.sort_by { _1["type"] }
|
|
260
|
+
.each do |definition|
|
|
261
|
+
type = definition["type"]
|
|
262
|
+
name = definition["name"]
|
|
263
|
+
description = definition["description"]
|
|
264
|
+
|
|
265
|
+
field_definitions = definition["fieldDefinitions"]&.map do |field|
|
|
266
|
+
field_hash = {
|
|
267
|
+
key: field["key"].to_sym,
|
|
268
|
+
type: field["type"]["name"].to_sym,
|
|
269
|
+
name: field["name"]
|
|
270
|
+
}
|
|
271
|
+
field_hash[:description] = field["description"] if field["description"] && !field["description"].empty?
|
|
272
|
+
field_hash[:required] = field["required"] if field["required"] == true
|
|
273
|
+
|
|
274
|
+
# Convert validations for metaobject reference fields within metaobjects
|
|
275
|
+
if field["validations"]&.any? && is_metaobject_reference_type?(field["type"]["name"])
|
|
276
|
+
field_hash[:validations] = convert_validations_gids_to_types(field["validations"], field["type"]["name"])&.map { |v| v.transform_keys(&:to_sym) }
|
|
277
|
+
elsif field["validations"]&.any?
|
|
278
|
+
field_hash[:validations] = field["validations"]&.map { |v| v.transform_keys(&:to_sym) }
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
field_hash
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
access = definition["access"]
|
|
285
|
+
capabilities = definition["capabilities"]
|
|
286
|
+
|
|
287
|
+
args = [type.to_sym]
|
|
288
|
+
kwargs = { name: name }
|
|
289
|
+
kwargs[:description] = description if description && !description.empty?
|
|
290
|
+
kwargs[:field_definitions] = field_definitions if field_definitions&.any?
|
|
291
|
+
kwargs[:access] = access if access&.any?
|
|
292
|
+
|
|
293
|
+
# Add capabilities if non-default
|
|
294
|
+
if capabilities&.any? { |_, v| v["enabled"] == true }
|
|
295
|
+
kwargs[:capabilities] = capabilities.transform_keys(&:to_sym).transform_values { |v| v.transform_keys(&:to_sym) }
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
args_string = args.map(&:inspect).join(", ")
|
|
299
|
+
kwargs_string = kwargs.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
|
|
300
|
+
content.puts " create_metaobject_definition #{args_string}, #{kwargs_string}"
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Add blank line between metaobjects and metafields if both exist
|
|
304
|
+
if metaobject_definitions.any? && metafield_definitions.any?
|
|
305
|
+
content.puts ""
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Add metafield definitions
|
|
309
|
+
metafield_definitions
|
|
137
310
|
.sort_by { [_1["ownerType"], _1["namespace"], _1["key"]] }
|
|
138
311
|
.each do
|
|
139
312
|
owner_type = _1["ownerType"].downcase.pluralize.to_sym
|
|
@@ -142,7 +315,7 @@ module ShopifyToolkit::Schema
|
|
|
142
315
|
name = _1["name"]
|
|
143
316
|
namespace = _1["namespace"]&.to_sym
|
|
144
317
|
description = _1["description"]
|
|
145
|
-
validations = _1["validations"]&.map { |v| v.transform_keys(&:to_sym) }
|
|
318
|
+
validations = convert_validations_gids_to_types(_1["validations"], type)&.map { |v| v.transform_keys(&:to_sym) }
|
|
146
319
|
capabilities =
|
|
147
320
|
_1["capabilities"]
|
|
148
321
|
&.transform_keys(&:to_sym)
|
|
@@ -152,10 +325,10 @@ module ShopifyToolkit::Schema
|
|
|
152
325
|
kwargs = { name: name }
|
|
153
326
|
kwargs[:namespace] = namespace if namespace && namespace != :custom
|
|
154
327
|
kwargs[:description] = description if description
|
|
155
|
-
kwargs[:validations] = validations if validations
|
|
328
|
+
kwargs[:validations] = validations if validations&.any?
|
|
156
329
|
|
|
157
330
|
# Only include capabilities if they have non-default values
|
|
158
|
-
if capabilities
|
|
331
|
+
if capabilities&.any?
|
|
159
332
|
has_non_default_capabilities =
|
|
160
333
|
capabilities.any? do |cap, value|
|
|
161
334
|
case cap
|
|
@@ -176,4 +349,43 @@ module ShopifyToolkit::Schema
|
|
|
176
349
|
content.puts "end"
|
|
177
350
|
content.string
|
|
178
351
|
end
|
|
352
|
+
|
|
353
|
+
def execute_metaobject_definitions(schema_content)
|
|
354
|
+
# Create a filtered version that only includes metaobject definitions
|
|
355
|
+
metaobject_content = filter_schema_content(schema_content, :metaobject)
|
|
356
|
+
eval_schema_content(metaobject_content)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def execute_metafield_definitions(schema_content)
|
|
360
|
+
# Create a filtered version that only includes metafield definitions
|
|
361
|
+
metafield_content = filter_schema_content(schema_content, :metafield)
|
|
362
|
+
eval_schema_content(metafield_content)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def filter_schema_content(schema_content, type)
|
|
366
|
+
lines = schema_content.lines
|
|
367
|
+
filtered_lines = []
|
|
368
|
+
|
|
369
|
+
# Always include the header and footer
|
|
370
|
+
filtered_lines << lines.first(8) # Header lines up to "ShopifyToolkit::Schema.define do"
|
|
371
|
+
filtered_lines.flatten!
|
|
372
|
+
|
|
373
|
+
lines.each do |line|
|
|
374
|
+
case type
|
|
375
|
+
when :metaobject
|
|
376
|
+
if line.strip.start_with?("create_metaobject_definition")
|
|
377
|
+
filtered_lines << line
|
|
378
|
+
end
|
|
379
|
+
when :metafield
|
|
380
|
+
filtered_lines << line if line.strip.start_with?("create_metafield")
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
filtered_lines << "end\n" # Closing line
|
|
385
|
+
filtered_lines.join
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def eval_schema_content(content)
|
|
389
|
+
instance_eval(content)
|
|
390
|
+
end
|
|
179
391
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: shopify_toolkit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Elia Schito
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: exe
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2025-
|
|
12
|
+
date: 2025-10-31 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: railties
|
|
@@ -114,6 +114,7 @@ files:
|
|
|
114
114
|
- lib/shopify_toolkit/command_line.rb
|
|
115
115
|
- lib/shopify_toolkit/metafield_statements.rb
|
|
116
116
|
- lib/shopify_toolkit/metaobject_statements.rb
|
|
117
|
+
- lib/shopify_toolkit/metaobject_utilities.rb
|
|
117
118
|
- lib/shopify_toolkit/migration.rb
|
|
118
119
|
- lib/shopify_toolkit/migration/logging.rb
|
|
119
120
|
- lib/shopify_toolkit/migrator.rb
|
|
@@ -144,7 +145,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
144
145
|
- !ruby/object:Gem::Version
|
|
145
146
|
version: '0'
|
|
146
147
|
requirements: []
|
|
147
|
-
rubygems_version: 3.5.
|
|
148
|
+
rubygems_version: 3.5.16
|
|
148
149
|
signing_key:
|
|
149
150
|
specification_version: 4
|
|
150
151
|
summary: A collection of tools for dealing with Shopify apps.
|