Skip to content
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,40 @@ ShopifyToolkit::MetafieldSchema.define do
end
```

### Monkey Patching for Specific Use Cases

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.

```rb
# config/initializers/shopify_toolkit_patches.rb

require "shopify_toolkit/metaobject_statements"

module ShopifyToolkit
module MetaobjectStatements
# Override create_metaobject_definition to skip Shopify-managed definitions
alias_method :original_create_metaobject_definition, :create_metaobject_definition

def create_metaobject_definition(type, **options)
# Skip Shopify-managed metaobject definitions during schema load
if type.to_s.start_with?("shopify--")
say "Skipping Shopify-managed metaobject definition: #{type}"
return
end

original_create_metaobject_definition(type, **options)
end
end
end
```

This approach allows you to:

- Skip creation of reserved metaobject types (e.g., those prefixed with `shopify--`)
- Handle cases where certain metaobjects cannot be created due to API limitations
- Customize validation logic for specific metaobject types
- Filter out problematic metafield references during schema loading

### Analyzing a Matrixify CSV Result files

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.
Expand Down
21 changes: 21 additions & 0 deletions lib/shopify_toolkit/metafield_statements.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module ShopifyToolkit::MetafieldStatements
extend ActiveSupport::Concern
include ShopifyToolkit::Migration::Logging
include ShopifyToolkit::AdminClient
include ShopifyToolkit::MetaobjectUtilities

def self.log_time(method_name)
current_method = instance_method(method_name)
Expand All @@ -26,6 +27,21 @@ def create_metafield(owner_type, key, type, namespace: :custom, name:, **options
return
end

# Process validations to convert metaobject types to GIDs (only for metaobject reference fields)
if options[:validations] && is_metaobject_reference_type?(type)
begin
options[:validations] = convert_validations_types_to_gids(options[:validations])
rescue RuntimeError => e
if e.message.include?("not found")
say "ERROR: Cannot create metafield #{namespace}:#{key} - references non-existent metaobject. This suggests the metaobject was filtered out or failed to create."
say " Original error: #{e.message}"
return
else
raise e
end
end
end

# https://shopify.dev/docs/api/admin-graphql/2024-10/mutations/metafieldDefinitionCreate
query =
"# GraphQL
Expand Down Expand Up @@ -117,6 +133,11 @@ def update_metafield(owner_type, key, namespace: :custom, **options)
return
end

# Process validations to convert metaobject types to GIDs (only for metaobject reference fields)
if options[:validations] && options[:type] && is_metaobject_reference_type?(options[:type])
options[:validations] = convert_validations_types_to_gids(options[:validations])
end

shopify_admin_client
.query(
# Documentation: https://shopify.dev/docs/api/admin-graphql/2024-10/mutations/metafieldDefinitionUpdate
Expand Down
166 changes: 142 additions & 24 deletions lib/shopify_toolkit/metaobject_statements.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ module ShopifyToolkit::MetaobjectStatements
extend ActiveSupport::Concern
include ShopifyToolkit::Migration::Logging
include ShopifyToolkit::AdminClient
include ShopifyToolkit::MetaobjectUtilities

@@pending_field_validations = []

def self.log_time(method_name)
current_method = instance_method(method_name)
Expand All @@ -14,18 +17,152 @@ def self.log_time(method_name)
end
end

# create_metafield :products, :my_metafield, :single_line_text_field, name: "Prova"
# @param namespace: if nil the metafield will be app-specific (default: :custom)
def apply_pending_field_validations
return if @@pending_field_validations.empty?

say "Applying #{@@pending_field_validations.size} pending field validations"

@@pending_field_validations.reject! do |item|
metaobject_type = item[:metaobject_type]
field_key = item[:field_key]
validations = item[:validations]

begin
success = add_field_validations_to_metaobject(metaobject_type, field_key, validations, item)
unless success
say "-- Deferring field '#{field_key}' in '#{metaobject_type}' (missing dependencies)"
end
success
rescue StandardError => e
say "-- Failed to process field '#{field_key}' in '#{metaobject_type}': #{e.message}"
true # Remove from array (don't retry errors)
end
end
end

log_time \
def create_metaobject_definition(type, **options)
# Skip creation if metafield already exists
# Skip creation if metaobject already exists
existing_gid = get_metaobject_definition_gid(type)
if existing_gid
say "Metaobject #{type} already exists, skipping creation"
return existing_gid
end

# https://shopify.dev/docs/api/admin-graphql/2024-10/mutations/metafieldDefinitionCreate
# Transform options for GraphQL API
definition = options.merge(type: type.to_s)

begin
# Convert field_definitions to fieldDefinitions and transform field structure
if options[:field_definitions]
definition[:fieldDefinitions] = options[:field_definitions].filter_map do |field|
field_def = build_field_definition(field)
field_needs_validations = is_metaobject_reference_type?(field[:type])

if field[:validations]
begin
converted_validations = convert_validations_types_to_gids(field[:validations])
field_def[:validations] = converted_validations if converted_validations&.any?
rescue RuntimeError => e
if e.message.include?("not found")
@@pending_field_validations << {
metaobject_type: type,
field_key: field[:key],
field_definition: field,
validations: field[:validations]
}
say "Deferring field '#{field[:key]}' in '#{type}' (missing dependency)"

if field_needs_validations
next # Skip this field entirely
end
end
end
elsif field_needs_validations
next # Skip fields that need validations but don't have any
end

field_def
end

definition.delete(:field_definitions)
end

# Remove admin access to avoid API restrictions
if definition[:access]&.is_a?(Hash)
definition[:access] = definition[:access].dup
definition[:access].delete("admin") if definition[:access]["admin"]
definition.delete(:access) if definition[:access].empty?
end

# Clean up empty validations arrays that cause API errors
if definition[:fieldDefinitions]
definition[:fieldDefinitions].each do |field_def|
field_def.delete(:validations) if field_def[:validations]&.empty?
end
end

result = create_metaobject_definition_immediate(definition)
result
end
end

def add_field_validations_to_metaobject(metaobject_type, field_key, validations, item = nil)
# Get the existing metaobject definition
existing_gid = get_metaobject_definition_gid(metaobject_type)
unless existing_gid
say "Error: Cannot add validations to '#{metaobject_type}' - metaobject not found"
return false
end

begin
converted_validations = convert_validations_types_to_gids(validations)

if converted_validations&.any?
# Use the passed item, or try to find it (for backward compatibility)
if item.nil?
item = @@pending_field_validations.find { |pending_item|
pending_item[:metaobject_type] == metaobject_type && pending_item[:field_key] == field_key
}
end

if item && item[:field_definition]
field_def = item[:field_definition]
new_field = build_field_definition(field_def, converted_validations)

field_operation = { create: new_field }
update_metaobject_definition(metaobject_type, fieldDefinitions: [field_operation])

say "Added field '#{field_key}' to '#{metaobject_type}'"
return true
else
return false
end
else
return false
end

rescue RuntimeError
return false # Keep trying later or don't retry errors
end
end

private

def build_field_definition(field, validations = nil)
field_def = {
key: field[:key].to_s,
name: field[:name],
type: field[:type].to_s
}
field_def[:description] = field[:description] if field[:description]
field_def[:required] = field[:required] if field[:required]
field_def[:validations] = validations if validations&.any?
field_def
end

def create_metaobject_definition_immediate(definition)
# https://shopify.dev/docs/api/admin-graphql/2024-10/mutations/metaobjectDefinitionCreate
query =
"# GraphQL
mutation CreateMetaobjectDefinition($definition: MetaobjectDefinitionCreateInput!) {
Expand All @@ -47,32 +184,13 @@ def create_metaobject_definition(type, **options)
}
}
"
variables = { definition: { type:, **options } }
variables = { definition: definition }

shopify_admin_client
.query(query:, variables:)
.tap { handle_shopify_admin_client_errors(_1, "data.metaobjectDefinitionCreate.userErrors") }
end

def get_metaobject_definition_gid(type)
result =
shopify_admin_client
.query(
query:
"# GraphQL
query GetMetaobjectDefinitionID($type: String!) {
metaobjectDefinitionByType(type: $type) {
id
}
}",
variables: { type: type.to_s },
)
.tap { handle_shopify_admin_client_errors(_1) }
.body

result.dig("data", "metaobjectDefinitionByType", "id")
end

def update_metaobject_definition(type, **options)
existing_gid = get_metaobject_definition_gid(type)

Expand Down
107 changes: 107 additions & 0 deletions lib/shopify_toolkit/metaobject_utilities.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# frozen_string_literal: true

require "active_support/concern"

module ShopifyToolkit::MetaobjectUtilities
extend ActiveSupport::Concern
include ShopifyToolkit::AdminClient

def is_metaobject_reference_type?(type)
type.to_sym.in?([:metaobject_reference, :"list.metaobject_reference", :mixed_reference, :"list.mixed_reference"])
end

def convert_validations_types_to_gids(validations)
return validations unless validations&.any?

validations.filter_map do |validation|
convert_metaobject_validation(validation)
end
end

def get_metaobject_definition_gid(type)
result =
shopify_admin_client
.query(
query:
"# GraphQL
query GetMetaobjectDefinitionID($type: String!) {
metaobjectDefinitionByType(type: $type) {
id
}
}",
variables: { type: type.to_s },
)
.tap { handle_shopify_admin_client_errors(_1) }
.body

gid = result.dig("data", "metaobjectDefinitionByType", "id")

return gid
end

def get_metaobject_definition_type_by_gid(gid)
result =
shopify_admin_client
.query(
query:
"# GraphQL
query GetMetaobjectDefinitionType($id: ID!) {
metaobjectDefinition(id: $id) {
type
}
}",
variables: { id: gid },
)
.tap { handle_shopify_admin_client_errors(_1) }
.body

result.dig("data", "metaobjectDefinition", "type")
end

private

def convert_metaobject_validation(validation)
name = validation[:name] || validation["name"]
value = validation[:value] || validation["value"]

return validation unless metaobject_type_validation?(name) && value

parsed_value = parse_json_if_needed(value)
convert_types_to_gids(parsed_value)
end

def metaobject_type_validation?(name)
name.in?(["metaobject_definition_type", "metaobject_definition_types"])
end

def parse_json_if_needed(value)
return value unless value.is_a?(String) && value.start_with?("[") && value.end_with?("]")

JSON.parse(value)
rescue JSON::ParserError
value
end

def convert_types_to_gids(value)
if value.is_a?(Array)
convert_array_types_to_gids(value)
else
convert_single_type_to_gid(value)
end
end

def convert_array_types_to_gids(types)
gids = types.map do |type|
gid = get_metaobject_definition_gid(type)
raise "Metaobject type '#{type}' not found" unless gid
gid
end
{ name: "metaobject_definition_ids", value: gids.to_json }
end

def convert_single_type_to_gid(type)
gid = get_metaobject_definition_gid(type)
raise "Metaobject type '#{type}' not found" unless gid
{ name: "metaobject_definition_id", value: gid }
end
end
1 change: 1 addition & 0 deletions lib/shopify_toolkit/migrator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
class ShopifyToolkit::Migrator # :nodoc:
include ShopifyToolkit::AdminClient
include ShopifyToolkit::MetafieldStatements
include ShopifyToolkit::MetaobjectStatements

singleton_class.attr_accessor :migrations_paths
self.migrations_paths = ["config/shopify/migrate"]
Expand Down
Loading