@Alan_G
API Version is 2025-01. This appears to be an issue only when Uninstalling and re-installing my app I believe. I do run into it a bit when running tests on pre-deployed versions using the Shopify CLI but that is a little intermittent so not sure if that is just a side-effect of using the dev environment.
As far as creating the metafields in the shopify.app.toml, that would be ideal however in testing, it appears that toml created metafields cannot be pinned which, for my application is necessary. Unless you’re able to provide an example of toml code that creates a metafield that can be pinned. That would be great.
Here is a snippet from the shopify.app.toml
[metaobjects.app.rackgrp_builder_options]
name = "Product Builder Option"
description = "Choices customers can make to customer/build their product."
display_name_field = "admin_label"
[metaobjects.app.rackgrp_builder_options.access]
admin = "merchant_read_write"
storefront = "public_read"
[metaobjects.app.rackgrp_builder_options.capabilities]
publishable = true
[metaobjects.app.rackgrp_builder_options.fields.admin_label]
name = "Display Name"
type = "single_line_text_field"
description = "For organizational purposes. Not visible to the public."
[metaobjects.app.rackgrp_builder_options.fields.label]
name = "Label"
type = "single_line_text_field"
description = "Label/Heading shown to the public."
[metaobjects.app.rackgrp_builder_options.fields.template]
name = "Template"
type = "single_line_text_field"
description = "How the options are displayed on the website."
[metaobjects.app.rackgrp_builder_options.fields.template.validations]
choices = [
"List",
"Small Thumbnails",
"Large Thumbnails"
]
[metaobjects.app.rackgrp_builder_options.fields.options]
name = "Options"
type = "list.single_line_text_field"
description = "Non-inventory options (eg. 'None' or 'Diameter')."
[metaobjects.app.rackgrp_builder_options.fields.products]
name = "Products"
type = "list.product_reference"
description = "Products the customer can choose from for this option."
[metaobjects.app.rackgrp_builder_options.fields.hide_price]
name = "Hide Price/Included"
type = "boolean"
description = "Do not show individual pricing or 'Included' for products in this group."
[metaobjects.app.rackgrp_builder_options.fields.default_variant_behavior]
name = "Default Variant Behavior"
type = "single_line_text_field"
description = "How the builder treats default and non-default choices."
[metaobjects.app.rackgrp_builder_options.fields.default_variant_behavior.validations]
choices = [
"Included In price",
"Addition to price",
"Excluded from order"
]
[metaobjects.app.rackgrp_builder_options.fields.included_variants]
name = "Included Variants"
type = "list.variant_reference"
description = "Products that are automatically added to the order along with chosen product options."
[metaobjects.app.rackgrp_builder_options.fields.z_index]
name = "Z-index"
type = "number_integer"
description = "Used to layer preview images. Higher number will be displayed over a lower number."
…and here is a snippet from my app JS that creates the metafields. You’ll notice that I am hard-coding the metafield targetHandles to match the ID’s in my app but I have tried not doing that and I still have the same issues.
let { ownerType, namespace, key, name, type, description, isDynamic } = def;
try {
const checkQuery = `
query {
metafieldDefinitions(first: 1, ownerType: ${ownerType}, namespace: "${namespace}", key: "${key}") {
edges { node { id name description } }
}
}
`;
const checkResJson = await shopifyGraphqlFetch(shop, accessToken, checkQuery);
const existing = checkResJson?.data?.metafieldDefinitions?.edges[0]?.node;
console.log(`[Metafields] Checking '${namespace}.${key}': ${existing ? "FOUND (ID: " + existing.id + ")" : "NOT FOUND"}`);
if (existing) {
if (existing.name !== name || existing.description !== description) {
console.log(`[Metafields] Updating '${namespace}.${key}'...`);
const updateMutation = `
mutation {
metafieldDefinitionUpdate(id: "${existing.id}", definition: { name: "${name}", description: "${description}" }) {
userErrors { message }
}
}
`;
await shopifyGraphqlFetch(shop, accessToken, updateMutation);
}
} else {
console.log(`[Metafields] Creating '${namespace}.${key}'...`);
let validations = "";
if (isDynamic) {
const targetHandles = ["app--290631450625--rackgrp_builder_template", "app--290631450625--rackgrp_builder_options", "app--290631450625--rackgrp_builder_field", "app--290631450625--rackgrp_builder_content"];
const gids = targetHandles.map(h => gidMap[h]).filter(Boolean);
validations = `, validations: [{name: "metaobject_definition_ids", value: "${JSON.stringify(gids).replace(/"/g, '\\"')}"}]`;
}
const createMutation = `
mutation {
metafieldDefinitionCreate(definition: {
name: "${name}", namespace: "${namespace}", key: "${key}",
ownerType: ${ownerType}, type: "${type}", description: "${description}" ${validations}
}) {
userErrors { message }
}
}
`;
const createResJson = await shopifyGraphqlFetch(shop, accessToken, createMutation);
const errors = createResJson?.data?.metafieldDefinitionCreate?.userErrors;
if (errors && errors.length > 0) {
console.error(`[Metafields] Creation error for '${namespace}.${key}':`, errors);
allDefinitionsSuccessful = false;
}
}
} catch (error) {
console.error(`[Metafields] Reconciliation error on ${namespace}.${key}:`, error.message);
allDefinitionsSuccessful = false;
}