Skip to content

Instantly share code, notes, and snippets.

@iamkevingreen
Last active October 20, 2023 06:55
Show Gist options
  • Star 44 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save iamkevingreen/3872f5acbaf5b100a8ebc02e4d95a03e to your computer and use it in GitHub Desktop.
Save iamkevingreen/3872f5acbaf5b100a8ebc02e4d95a03e to your computer and use it in GitHub Desktop.
const sanityClient = require('@sanity/client');
const crypto = require('crypto');
const {
SANITY_API_TOKEN,
SANITY_PROJECT_ID,
SANITY_DATASET,
SHOPIFY_SECRET
} = process.env;
const client = sanityClient({
projectId: SANITY_PROJECT_ID,
dataset: SANITY_DATASET,
token: SANITY_API_TOKEN,
});
exports.handler = async (event, context) => {
if (event.httpMethod !== 'POST' || !event.body) {
return {
statusCode: 400,
body: ''
};
}
let data;
let hasVariantsToSync = false;
const hmac = event.headers['x-shopify-hmac-sha256']
try {
data = JSON.parse(event.body);
const generatedHash = crypto
.createHmac('sha256', SHOPIFY_SECRET)
.update(event.body)
.digest('base64')
if (generatedHash !== hmac) {
return {
statusCode: 400,
body: JSON.stringify({
error: 'Bad Webhook Request'
})
}
}
} catch (error) {
console.log('JSON parsing error:', error);
return {
statusCode: 400,
body: JSON.stringify({
error: 'Bad request body'
})
};
}
// Build our initial product
const product = {
_type: 'product',
_id: data.id.toString(),
productId: data.id,
title: data.title,
defaultPrice: data.variants[0].price,
slug: {
_type: 'slug',
current: data.handle
}
};
return client
.transaction()
.createIfNotExists(product)
.patch(data.id.toString(), patch => patch.set(product))
.commit()
.then(res => {
console.log(`Successfully updated/patched Product ${data.id} in Sanity`);
if (data.variants.length > 1) {
hasVariantsToSync = true;
return Promise.all(data.variants.map(variant => {
const variantData = {
_type: 'productVariant',
_id: variant.id.toString(),
productId: data.id,
variantId: variant.id,
title: data.title,
variantTitle: variant.title,
sku: variant.sku,
price: variant.price
};
return client
.transaction()
.createIfNotExists(variantData)
.patch(variant.id.toString(), patch => patch.set(variantData))
.commit()
.then(response => {
console.log(`Successfully updated/patched Variant ${variant.id} in Sanity`);
return response;
})
.catch(error => {
console.log('Sanity error:', error);
return error;
});
})).then(result => {
if (hasVariantsToSync) {
return client
.transaction()
.createIfNotExists(product)
.patch(data.id.toString(), patch => patch.set({
variants: data.variants.map(variant => ({
_type: 'reference',
_ref: variant.id.toString(),
_key: variant.id.toString(),
}))
}))
.commit()
.then(response => {
console.log(`Successfully added variant references to ${data.id} in Sanity`);
return {
statusCode: 200,
body: JSON.stringify(response)
};
})
.catch(error => {
console.log('Sanity error:', error);
return error;
});
} else {
return {
statusCode: 200,
body: JSON.stringify(res)
};
}
}).catch(error => {
console.log('Sanity error:', error);
return {
statusCode: 500,
body: JSON.stringify({
error: 'An internal server error has occurred',
})
};
});
} else {
return {
statusCode: 200,
body: JSON.stringify(res)
};
}
})
.catch(error => {
console.log('Sanity error:', error);
return {
statusCode: 500,
body: JSON.stringify({
error: 'An internal server error has occurred',
})
};
});
};
@homerjam
Copy link

homerjam commented Jan 15, 2020

Hi @iamkevingreen - I found this via the recent roundtable video, it's gonna be super useful as I'm starting out my first Shopify + Sanity project. Something that I didn't pick up from the video - how/when is this function triggered? What's the content of the request body and where does it come from? Any help is much appreciated - thanks!

@iamkevingreen
Copy link
Author

@homerjam - So this is a serverless function, in my case it lives in netlify so the route is : https://site.com/.netlify/functions/shopify

The payload is coming from a shopify webhook. Which you can set up in Shopify->Settings->Notifications. You'll want to simply add the route above, to the product create & update with a JSON payload. After that everytime a product is updated/created the payload will hit this function and save data into your Sanity instance.

@homerjam
Copy link

Awesome. Thanks

@alexcasche
Copy link

alexcasche commented Oct 14, 2020

I noticed that this webhook fires after every product sale. Any workaround here? It's causing our server to receive a ton of product/update webhooks we don't care about. Here is a thread discussing the issue:
https://community.shopify.com/c/Shopify-APIs-SDKs/Will-products-update-webhook-always-fire-when-an-inventory-level/td-p/572280

@iamkevingreen
Copy link
Author

@alexcasche - what you are noticing is... a lovely feature of the Shopify Webhook, in which they do indeed trigger the rebuild with the inventory, and they do not specify in that webhook that the only thing that changed is inventory, and yes that thread discusses it further. I'm actually familiar with that post :)

ANYWAY. I have a temp working example that you could rift off here: https://github.com/ctrl-alt-del-world/midway/blob/master/web/src/lambda/shopify-sync.ts#L218

I now do a fetch directly to sanity and do a simple diff on the title/handle/image/price and if any of those things change I trigger the transaction, if not it skips the update.

@alexcasche
Copy link

alexcasche commented Oct 14, 2020 via email

@iamkevingreen
Copy link
Author

@alexcasche yeah I wish there was a better way to diff compare without a third party, and since shopify no longer sends inventory change it's pretty frustrating the hacks we need to implement to prevent the updates. But if I have to check sanity before updating i'd prefer that and prevent the rebuilding. I am experimenting with a different way to handle it that can also scale for users with hopefully low overhead. Also looking into Fauna as an option as it works pretty well with serverless and has pretty decent free plans.

@alexcasche
Copy link

@iamkevingreen yeah right now we’re doing SSR so we can handle some server tasks. But netlify add ons could easily replace the need soon. Background jobs and some redis integration would be awesome. Fauna looks really cool too

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment