-
Notifications
You must be signed in to change notification settings - Fork 0
Add portable metaobject definitions and references to Shopify Toolkit schema dump #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
say "Metafield #{namespace}:#{key} already exists for #{owner_type}, skipping creation" | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. Positive FeedbackNegative Feedback |
||
|
||
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" | ||
|
||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. Positive FeedbackNegative Feedback |
||
return | ||
|
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -8,6 +8,7 @@ | |||||||||
module ShopifyToolkit::Schema | ||||||||||
extend self | ||||||||||
include ShopifyToolkit::MetafieldStatements | ||||||||||
include ShopifyToolkit::MetaobjectStatements | ||||||||||
include ShopifyToolkit::Migration::Logging | ||||||||||
|
||||||||||
delegate :logger, to: Rails | ||||||||||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||
def fetch_definitions(owner_type:) | ||||||||||
owner_type = owner_type.to_s.singularize.upcase | ||||||||||
|
||||||||||
|
@@ -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 | ||||||||||
|
@@ -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) | ||||||||||
|
@@ -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 | ||||||||||
|
There was a problem hiding this comment.
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.