import { Card, Page, Text } from "@shopify/polaris";
import { authenticate } from "../shopify.server";
import { PrismaClient } from "@prisma/client";
import { useLoaderData } from "@remix-run/react";
// Initialize Prisma client
const prisma = new PrismaClient();
export const loader = async ({ request }) => {
// Authenticate the request to get the session and admin client
const { session, admin } = await authenticate.admin(request);
const shop = session.shop;
// Query the Session table to get the accessToken for the shop
const sessionRecord = await prisma.session.findFirst({
where: { shop },
select: { accessToken: true },
});
if (!sessionRecord) {
throw new Error(`No session found for shop: ${shop}`);
}
const accessToken = sessionRecord.accessToken;
// Fetch all products from Shopify Admin API
const apiUrl = `https://${shop}/admin/api/2025-07/products.json`;
try {
const response = await fetch(apiUrl, {
method: "GET",
headers: {
"X-Shopify-Access-Token": accessToken,
},
});
if (!response.ok) {
throw new Error(`Failed to fetch products: ${response.statusText}`);
}
const data = await response.json();
const products = data.products || [];
// Check if metafields have already been processed for this shop
const shopMetafieldCheck = await admin.graphql(
`
query {
shop {
metafield(namespace: "app_config", key: "metafields_initialized") {
value
}
}
}
`,
);
const shopMetafieldData = await shopMetafieldCheck.json();
const metafieldsInitialized =
shopMetafieldData.data?.shop?.metafield?.value === "true";
if (!metafieldsInitialized) {
// GraphQL query to check existing metafields for a product
const metafieldQuery = `
query($id: ID!) {
product(id: $id) {
id
title
metafield(namespace: "shipping", key: "flat_rate") {
id
value
}
}
}
`;
// GraphQL mutation to update product with metafield
const mutation = `
mutation productUpdate($input: ProductInput!) {
productUpdate(input: $input) {
product {
id
title
metafield(namespace: "shipping", key: "flat_rate") {
id
namespace
key
value
type
}
}
userErrors {
field
message
}
}
}
`;
const errors = [];
const successes = [];
// Iterate over products to check and create metafields
for (const product of products) {
try {
// Convert REST API product ID to GraphQL GID
const productGid = `gid://shopify/Product/${product.id}`;
// Check if the product already has the metafield
const metafieldCheck = await admin.graphql(metafieldQuery, {
variables: { id: productGid },
});
const metafieldData = await metafieldCheck.json();
const existingMetafield = metafieldData.data?.product?.metafield;
// Only create the metafield if it doesn't exist
if (!existingMetafield) {
const fieldResponse = await admin.graphql(mutation, {
variables: {
input: {
id: productGid,
metafields: [
{
namespace: "shipping",
key: "flat_rate",
value: "0.0",
type: "number_decimal",
},
],
},
},
});
const responseData = await fieldResponse.json();
const userErrors =
responseData.data?.productUpdate?.userErrors || [];
if (userErrors.length > 0) {
errors.push({
productId: product.id,
title: product.title,
errors: userErrors,
});
} else {
successes.push({
productId: product.id,
title: product.title,
metafield: responseData.data?.productUpdate?.product?.metafield,
});
}
} else {
// Optionally track products that already had the metafield
successes.push({
productId: product.id,
title: product.title,
metafield: existingMetafield,
message: "Metafield already exists",
});
}
} catch (error) {
errors.push({
productId: product.id,
title: product.title,
errors: [{ message: error.message }],
});
}
}
// Mark the shop as initialized by setting a shop-level metafield
if (errors.length === 0 || successes.length > 0) {
await admin.graphql(
`
mutation shopMetafieldsSet($metafields: [MetafieldsSetInput!]!) {
metafieldsSet(metafields: $metafields) {
metafields {
id
namespace
key
value
}
userErrors {
field
message
}
}
}
`,
{
variables: {
metafields: [
{
namespace: "app_config",
key: "metafields_initialized",
value: "true",
type: "boolean",
ownerId: `gid://shopify/Shop/${session.shopId}`,
},
],
},
},
);
}
return {
accessToken,
products,
successes,
errors: errors.length > 0 ? errors : null,
};
}
return { accessToken, products, successes: [], errors: null };
} catch (error) {
console.error("Error in loader:", error);
return {
accessToken,
products: [],
successes: [],
errors: [{ message: error.message }],
};
}
};
export default function Index() {
// Retrieve data from loader
const { successes, errors } = useLoaderData();
return (
<Page title="Product Shipping Flat Rate App">
<Card>
<Text variant="headingMd" as="h2">
Welcome
</Text>
<Text as="p">
This app has automatically set a flat shipping rate metafield for
products that did not already have it.
</Text>
{errors && (
<Text as="p" tone="critical">
Errors occurred during metafield creation:
{errors.map((error, index) => (
<div key={index}>
Product {error.title} (ID: {error.productId}):{" "}
{error.errors.map((e) => e.message).join(", ")}
</div>
))}
</Text>
)}
{successes?.length > 0 && (
<Text as="p" tone="success">
Processed {successes.length} products. Metafields created or already
existed.
</Text>
)}
</Card>
</Page>
);
}
Hi, everyone i am creating an metafield using graphql its working fine but its creating unstructured metafield but i want to create an structured metafield how can i do that please help me with this. Thank you for you response in advance
Thank You.