Discuss all the new features introduced with the new product model in GraphQL.
I thought I might ask here as I wasn't getting any help in the Technical Forums.
Basically I have an API that gives me product data which I format and then use in the graphql to create a product. I need to create the product, add its variants and then the quantity available for each and its price.
I've set this up so far but I'm stuck at doing the last part of setting the price for the options and their qty's
//Preparing the Data
const prepareAmrodProducts = (products) => {
return products.map((product) => {
// Prepare color options
const colorOptions = product.variants.reduce((acc, variant) => {
if (variant.codeColourName && !acc.includes(variant.codeColourName)) {
acc.push(variant.codeColourName);
}
return acc;
}, []);
const sizeOptions = product.variants.reduce((acc, variant) => {
if (variant.codeSizeName && !acc.includes(variant.codeSizeName)) {
acc.push(variant.codeSizeName);
}
return acc;
}, []);
const productOptions = [];
if (colorOptions.length > 0) {
productOptions.push({
name: "Color",
values: colorOptions.map((color) => ({ name: color })),
});
}
if (sizeOptions.length > 0) {
productOptions.push({
name: "Size",
values: sizeOptions.map((size) => ({ name: size })),
});
}
return {
title: product.productName,
bodyHtml: product.description,
vendor: "Amrod",
productType: product.categories[0]?.name || "Default Category",
images: product.images.map((image) => ({
src: image.urls[0]?.url || "Default Image URL",
width: image.urls[0]?.width || 1024,
height: image.urls[0]?.height || 1024,
})),
productOptions: productOptions,
variants: product.variants.map((variant) => ({
price: variant.price || 0,
inventoryManagement: "SHOPIFY",
inventoryPolicy: "DENY",
sku: variant.fullCode,
inventoryQuantity: variant.stock || 0,
options: [
variant.codeColourName || "",
variant.codeSizeName || "",
].filter(Boolean),
})),
};
});
};
And here is how I attempt to create the product:
const createProductBatch = async (productsData) => {
// Product creation mutation
const productCreateMutation = `
mutation CreateProduct($input: ProductInput!, $media: [CreateMediaInput!]) {
productCreate(input: $input, media: $media) {
product {
id
options {
id
name
position
values
optionValues {
id
name
hasVariants
}
}
media(first: 10) {
nodes {
alt
mediaContentType
preview {
status
}
}
}
variants(first: 5) {
nodes {
id
title
selectedOptions {
name
value
}
}
}
metafields(first: 10) {
edges {
node {
type
namespace
key
value
}
}
}
}
userErrors {
field
message
}
}
}
`;
const batchSize = 5;
const delay = 500;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let results = [];
for (let i = 0; i < productsData.length; i += batchSize) {
const batch = productsData.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map(async (product) => {
//Media
const mediaInput = product.images.map((image) => ({
originalSource: image.src,
alt: image.altText || "",
mediaContentType: "IMAGE",
}));
// Create the product
const productResponse = await shopifyAPI.post(
"",
JSON.stringify({
query: productCreateMutation,
variables: {
input: {
title: product.title,
bodyHtml: product.bodyHtml,
vendor: product.vendor,
productType: product.productType,
metafields: [
{
namespace: "custom",
key: "sync_from_api",
type: "boolean",
value: "true",
},
],
productOptions: product.productOptions,
},
media: mediaInput,
},
})
);
if (!productResponse.data || productResponse.data.errors) {
console.error("GraphQL Error:", productResponse.data.errors);
return null;
}
if (productResponse.data.data.productCreate.userErrors.length > 0) {
console.error(
"User Errors:",
productResponse.data.data.productCreate.userErrors
);
return null;
}
// Product ID to use for creating variants
const productId = productResponse.data.data.productCreate.product.id;
return {
productId: productId,
};
})
);
results = results.concat(batchResults);
await sleep(delay);
}
return results.filter((result) => result !== null);
};
If anyone can give me some guidance on how to complete the last part or if I'm doing this correctly or wrong etc I'd really appreciate it. Even if you have some resource to teach me in an easy manor.
Hi @Demonix,
Using the new Product API Model, once you create the Product with multiple Product Options, as you are doing here, you will then need to create the variants in a second API call. This is since the productCreate mutation when created with multiple Product Options, will create the Product Options, but doesn't actually create variants for them yet, however it does create a single default variant using the first listed product option values, and any additional variants will need to be created with a productVariantsBulkCreate call.
For example if you are running the productCreate mutation with the following input:
{
"productInput": {
"title": "My cool socks",
"productOptions": [
{
"name": "Color",
"values": [
{
"name": "Red"
},
{
"name": "Green"
},
{
"name": "Blue"
}
]
},
{
"name": "Size",
"values": [
{
"name": "Small"
},
{
"name": "Medium"
},
{
"name": "Large"
}
]
}
]
}
}
This will create a Product called My cool socks, this Product will have Product Options: Color and Size, with Option Values: Red, Green, Blue, and Small, Medium, Large. This product will also only have a single default variant created with Red/Small product option values.
Once you have created the product, you will need to create the Product Variant, with the productVariantsBulkCreate call. In the input of the productVariantsBulkCreate call you can set the prices and inventory values for each variant you are creating.
Additionally since you already have the Red/Small variant, instead of having to create new variants, then having to update the Red/Small variant with prices and inventory, you can set strategy: REMOVE_STANDALONE_VARIANT in the initial productVariantsBulkCreate call and include the Red/Small variant in the productVariantsBulkCreate input. Here's an example for reference:
Call Body:
mutation productVariantsBulkCreate($productId: ID!, $strategy: ProductVariantsBulkCreateStrategy, $variants: [ProductVariantsBulkInput!]!) {
productVariantsBulkCreate(productId: $productId, strategy: $strategy, variants: $variants) {productVariants{
id
title
price
inventoryPolicy
inventoryQuantity
.... etc ....
}
}
}
Call Variables:
{
"productId": "gid://shopify/Product/1234567890",
"strategy": "REMOVE_STANDALONE_VARIANT",
"variants": [
{
"price": 4.99,
"inventoryPolicy": "DENY",
"inventoryQuantities": [
{
"availableQuantity": 10,
"locationId": "gid://shopify/Location/1234567890"
}
],
"inventoryItem": {
"cost":1.99,
"tracked": true,
"requiresShipping": true
},
"optionValues": [
{
"name": "Red",
"optionName": "Color"
},
{
"name": "Small",
"optionName": "Size"
}
]
},{
"price": 4.99,
"inventoryPolicy": "DENY",
"inventoryQuantities": [
{
"availableQuantity": 10,
"locationId": "gid://shopify/Location/1234567890"
}
],
"inventoryItem": {
"cost":1.99,
"tracked": true,
"requiresShipping": true
},
"optionValues": [
{
"name": "Red",
"optionName": "Color"
},
{
"name": "Medium",
"optionName": "Size"
}
]
},{
"price": 4.99,
"inventoryPolicy": "DENY",
"inventoryQuantities": [
{
"availableQuantity": 10,
"locationId": "gid://shopify/Location/1234567890"
}
],
"inventoryItem": {
"cost":1.99,
"tracked": true,
"requiresShipping": true
},
"optionValues": [
{
"name": "Red",
"optionName": "Color"
},
{
"name": "Large",
"optionName": "Size"
}
]
}
]
}
We do have some resources in our Shopify.dev documentation as well that does explain this in further detail, as well as providing guidance on using our new Product API Models.
I hope this helps, and I hope you have a great day 🙂
Developer Support @ Shopify
- Was this reply helpful? Click Like to let us know!
- Was your question answered? Mark it as an Accepted Solution
- To learn more visit Shopify.dev or the Shopify Web Design and Development Blog
Thanks for the breakdown. I've managed to setup an example for now to test and broke it up like this:
const exampleProduct = {
title: "Example Product",
bodyHtml: "Description of the example product.",
vendor: "Test Vendor",
productType: "ProductType",
images: [
{ src: "https://i.imgur.com/ynSF4sO.png", altText: "Image 1" },
{ src: "https://i.imgur.com/T0Q5Q5H.png", altText: "Image 2" },
],
variants: [
{
price: "19.99",
sku: "EXAMPLE-SKU-1",
inventoryQuantity: 100,
options: ["Red"],
},
{
price: "24.99",
sku: "EXAMPLE-SKU-2",
inventoryQuantity: 50,
options: ["Blue"],
},
],
};
const createProduct = async (product) => {
const productCreateMutation = `
mutation CreateProduct($input: ProductInput!, $media: [CreateMediaInput!]) {
productCreate(input: $input, media: $media) {
product {
id
}
userErrors {
field
message
}
}
}
`;
const mediaInput = product.images.map((image) => ({
originalSource: image.src,
alt: image.altText || "",
mediaContentType: "IMAGE",
}));
try {
const productResponse = await shopifyAPI.post(
"",
JSON.stringify({
query: productCreateMutation,
variables: {
input: {
title: product.title,
bodyHtml: product.bodyHtml,
vendor: product.vendor,
productType: product.productType,
metafields: [
{
namespace: "custom",
key: "sync_from_api",
type: "boolean",
value: "true",
},
],
},
media: mediaInput,
},
})
);
if (
productResponse.data.errors ||
productResponse.data.data.productCreate.userErrors.length > 0
) {
console.error(
"Error creating product:",
productResponse.data.errors ||
productResponse.data.data.productCreate.userErrors
);
return null;
}
return productResponse.data.data.productCreate.product.id;
} catch (error) {
console.error("Network or Server Error:", error);
return null;
}
};
const createVariants = async (productId, variants) => {
const variantCreateMutation = `
mutation CreateVariant($input: ProductVariantInput!) {
productVariantCreate(input: $input) {
productVariant {
id
inventoryItem {
id
}
}
userErrors {
field
message
}
}
}
`;
const createdVariants = [];
for (const variant of variants) {
try {
const variantResponse = await shopifyAPI.post(
"",
JSON.stringify({
query: variantCreateMutation,
variables: {
input: {
productId,
price: variant.price,
sku: variant.sku,
options: variant.options,
},
},
})
);
if (
variantResponse.data.errors ||
variantResponse.data.data.productVariantCreate.userErrors.length > 0
) {
console.error(
"Error creating variant:",
variantResponse.data.errors ||
variantResponse.data.data.productVariantCreate.userErrors
);
} else {
console.log(`Variant created: ${variant.sku}`);
const inventoryItemId =
variantResponse.data.data.productVariantCreate.productVariant.inventoryItem.id
.split("/")
.pop();
createdVariants.push({
inventoryItemId,
inventoryQuantity: variant.inventoryQuantity,
});
}
} catch (error) {
console.error("Network or Server Error:", error);
}
}
return createdVariants;
};
const adjustInventory = async (inventoryItemId, inventoryQuantity) => {
const restAPI = axios.create({
baseURL: `https://${process.env.SHOPIFY_SHOP}/admin/api/2024-04`,
headers: {
"Content-Type": "application/json",
"X-Shopify-Access-Token": process.env.SHOPIFY_ACCESS_TOKEN,
},
});
const locationId = process.env.SHOPIFY_LOCATION_ID;
try {
await restAPI.post(`/inventory_levels/adjust.json`, {
location_id: locationId,
inventory_item_id: inventoryItemId, // Ensure inventoryItemId is correct
available_adjustment: inventoryQuantity,
});
console.log(`Inventory adjusted for item: ${inventoryItemId}`);
} catch (error) {
console.error("Error adjusting inventory:", error.response.data);
}
};
const HandleShopProductCreation = async () => {
const productId = await createProduct(exampleProduct);
if (productId) {
console.log(`Successfully created product: ${productId}`);
const createdVariants = await createVariants(
productId,
exampleProduct.variants
);
for (const variant of createdVariants) {
await adjustInventory(variant.inventoryItemId, variant.inventoryQuantity); // Ensure inventoryItemId is correct
}
} else {
console.error("Failed to create product");
}
};
I see the only problem now is we are getting a default variant with a default title and the sales channel which seems to be needed isn't being added. I assume I need another mutation to do the sales channel?
Best
I've got the sales channel sorted now its just that default title as a option/variant I'm getting.
I also use the strategy to remove the standalone variant but still get the default variant, any solution..?