Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions lib/shopify_toolkit/metafield_statements.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ def self.log_time(method_name)
def create_metafield(owner_type, key, type, namespace: :custom, name:, **options)
ownerType = owner_type.to_s.singularize.upcase # Eg. "PRODUCT"

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

# Skip creation if metafield already exists
if get_metafield_gid(owner_type, key, namespace: namespace)
Comment on lines +23 to 29
Copy link

Copilot AI Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation conversion occurs before the existence check; if the metafield already exists and a referenced metaobject type is missing, this can raise unnecessarily. Move the conversion logic after the early-return existence check.

Copilot uses AI. Check for mistakes.

say "Metafield #{namespace}:#{key} already exists for #{owner_type}, skipping creation"
Expand Down Expand Up @@ -50,6 +55,39 @@ def create_metafield(owner_type, key, type, namespace: :custom, name:, **options
.tap { handle_shopify_admin_client_errors(_1, "data.metafieldDefinitionCreate.userErrors") }
end

def is_metaobject_reference_type?(type)
type_str = type.to_s
type_str == "metaobject_reference" || type_str == "list.metaobject_reference"
end
Comment on lines +58 to +61
Copy link

Copilot AI Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This duplicates the same logic in schema.rb; extract to a shared module (e.g., SchemaUtils) to keep a single authoritative definition.

Copilot uses AI. Check for mistakes.


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

validations.map do |validation|
validation_name = validation[:name] || validation["name"]
validation_value = validation[:value] || validation["value"]

if validation_name == "metaobject_definition_type" && validation_value
if validation_value.is_a?(Array)
# Handle array of types (for list.metaobject_reference)
gids = validation_value.map do |type|
gid = get_metaobject_definition_gid(type)
raise "Metaobject type '#{type}' not found" unless gid
gid
end
{ name: "metaobject_definition_id", value: gids }
else
# Handle single type
gid = get_metaobject_definition_gid(validation_value)
raise "Metaobject type '#{validation_value}' not found" unless gid
{ name: "metaobject_definition_id", value: gid }
end
else
validation
end
end
end

def get_metafield_gid(owner_type, key, namespace: :custom)
ownerType = owner_type.to_s.singularize.upcase # Eg. "PRODUCT"

Expand Down Expand Up @@ -112,6 +150,11 @@ def remove_metafield(owner_type, key, namespace: :custom, delete_associated_meta

log_time \
def update_metafield(owner_type, key, namespace: :custom, **options)
# 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

unless get_metafield_gid(owner_type, key, namespace: namespace)
say "Metafield #{namespace}:#{key} not found for #{owner_type}, skipping update"
Comment on lines +153 to 159
Copy link

Copilot AI Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation conversion (which can raise on missing types) is performed before confirming the metafield exists. Move the conversion inside the branch after the existence check to avoid unnecessary errors when skipping.

Copilot uses AI. Check for mistakes.

return
Expand Down
19 changes: 19 additions & 0 deletions lib/shopify_toolkit/metaobject_statements.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,25 @@ def get_metaobject_definition_gid(type)
result.dig("data", "metaobjectDefinitionByType", "id")
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

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

Expand Down
160 changes: 151 additions & 9 deletions lib/shopify_toolkit/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
module ShopifyToolkit::Schema
extend self
include ShopifyToolkit::MetafieldStatements
include ShopifyToolkit::MetaobjectStatements
include ShopifyToolkit::Migration::Logging

delegate :logger, to: Rails
Expand Down Expand Up @@ -69,6 +70,43 @@ def define(&block)
instance_eval(&block)
end

def convert_validations_gids_to_types(validations, metafield_type)
unless validations&.any? && is_metaobject_reference_type?(metafield_type)
return validations
end

validations.map do |validation|
if validation["name"] == "metaobject_definition_id"
value = validation["value"]

if value.is_a?(Array)
# Handle array of GIDs (for list.metaobject_reference)
types = value.filter_map do |gid|
if gid&.start_with?("gid://shopify/MetaobjectDefinition/")
get_metaobject_definition_type_by_gid(gid)
else
gid # Keep non-GID values as-is
end
end
validation.merge("name" => "metaobject_definition_type", "value" => types)
elsif value&.start_with?("gid://shopify/MetaobjectDefinition/")
# Handle single GID
type = get_metaobject_definition_type_by_gid(value)
validation.merge("name" => "metaobject_definition_type", "value" => type)
else
validation
end
else
validation
end
end
end

def is_metaobject_reference_type?(type)
type_str = type.to_s
type_str == "metaobject_reference" || type_str == "list.metaobject_reference"
end

Comment on lines +105 to +109
Copy link

Copilot AI Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate implementation of is_metaobject_reference_type? also exists in metafield_statements.rb; consolidate into a single shared helper to avoid divergence.

Suggested change
def is_metaobject_reference_type?(type)
type_str = type.to_s
type_str == "metaobject_reference" || type_str == "list.metaobject_reference"
end

Copilot uses AI. Check for mistakes.

def fetch_definitions(owner_type:)
owner_type = owner_type.to_s.singularize.upcase

Expand Down Expand Up @@ -116,24 +154,128 @@ def fetch_definitions(owner_type:)
result.dig("data", "metafieldDefinitions", "nodes") || []
end

def fetch_metaobject_definitions
query = <<~GRAPHQL
query {
metaobjectDefinitions(first: 250) {
nodes {
id
type
name
description
fieldDefinitions {
key
name
description
type {
name
}
required
validations {
name
value
}
}
access {
admin
storefront
}
capabilities {
publishable {
enabled
}
translatable {
enabled
}
}
}
}
}
GRAPHQL

result =
shopify_admin_client
.query(query:)
.tap { handle_shopify_admin_client_errors(_1) }
.body

result.dig("data", "metaobjectDefinitions", "nodes") || []
end

def generate_schema_content
definitions =
metaobject_definitions = fetch_metaobject_definitions
metafield_definitions =
OWNER_TYPES.flat_map { |owner_type| fetch_definitions(owner_type:) }

content = StringIO.new
content << <<~RUBY
# This file is auto-generated from the current state of the Shopify metafields.
# Instead of editing this file, please use the metafields migration feature of ShopifyToolkit
# to incrementally modify your metafields, and then regenerate this schema definition.
# This file is auto-generated from the current state of the Shopify metafields and metaobjects.
# Instead of editing this file, please use the migration features of ShopifyToolkit
# to incrementally modify your metafields and metaobjects, and then regenerate this schema definition.
#
# This file is the source used to define your metafields when running `bin/rails shopify:schema:load`.
#
# It's strongly recommended that you check this file into your version control system.
ShopifyToolkit::Schema.define do
RUBY

# Sort for consistent output
definitions
# Add metaobject definitions first
metaobject_definitions
.sort_by { _1["type"] }
.each do |definition|
type = definition["type"]
name = definition["name"]
description = definition["description"]

field_definitions = definition["fieldDefinitions"]&.map do |field|
field_hash = {
key: field["key"].to_sym,
type: field["type"]["name"].to_sym,
name: field["name"]
}
field_hash[:description] = field["description"] if field["description"] && !field["description"].empty?
field_hash[:required] = field["required"] if field["required"] == true

# Convert validations for metaobject reference fields within metaobjects
if field["validations"]&.any? && is_metaobject_reference_type?(field["type"]["name"])
field_hash[:validations] = convert_validations_gids_to_types(field["validations"], field["type"]["name"])&.map { |v| v.transform_keys(&:to_sym) }
elsif field["validations"]&.any?
field_hash[:validations] = field["validations"]&.map { |v| v.transform_keys(&:to_sym) }
end

field_hash
end

access = definition["access"]
capabilities = definition["capabilities"]

args = [type.to_sym]
kwargs = { name: name }
kwargs[:description] = description if description && !description.empty?
kwargs[:field_definitions] = field_definitions if field_definitions&.any?

# Add access if non-default
if access && (access["admin"] != true || access["storefront"] != true)
kwargs[:access] = access.transform_keys(&:to_sym)
end

# Add capabilities if non-default
if capabilities&.any? { |_, v| v["enabled"] == true }
kwargs[:capabilities] = capabilities.transform_keys(&:to_sym).transform_values { |v| v.transform_keys(&:to_sym) }
end

args_string = args.map(&:inspect).join(", ")
kwargs_string = kwargs.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
content.puts " create_metaobject_definition #{args_string}, #{kwargs_string}"
end

# Add blank line between metaobjects and metafields if both exist
if metaobject_definitions.any? && metafield_definitions.any?
content.puts ""
end

# Add metafield definitions
metafield_definitions
.sort_by { [_1["ownerType"], _1["namespace"], _1["key"]] }
.each do
owner_type = _1["ownerType"].downcase.pluralize.to_sym
Expand All @@ -142,7 +284,7 @@ def generate_schema_content
name = _1["name"]
namespace = _1["namespace"]&.to_sym
description = _1["description"]
validations = _1["validations"]&.map { |v| v.transform_keys(&:to_sym) }
validations = convert_validations_gids_to_types(_1["validations"], type)&.map { |v| v.transform_keys(&:to_sym) }
capabilities =
_1["capabilities"]
&.transform_keys(&:to_sym)
Expand All @@ -152,10 +294,10 @@ def generate_schema_content
kwargs = { name: name }
kwargs[:namespace] = namespace if namespace && namespace != :custom
kwargs[:description] = description if description
kwargs[:validations] = validations if validations.present?
kwargs[:validations] = validations if validations&.any?

# Only include capabilities if they have non-default values
if capabilities.present?
if capabilities&.any?
has_non_default_capabilities =
capabilities.any? do |cap, value|
case cap
Expand Down
Loading