Fetch Product variant by SKU works in storefront but not in independent JavaScript program - shopify

If I put below script in theme.liquid(Shopify storefront), I get expected result:
<script>
const GRAPHQL_URL = 'https://<my-store>/admin/variants/search.json?query=sku:big-mug-black';
const GRAPHQL_BODY = {
'method': 'GET',
'headers': {
'Content-Type': 'application/json',
},
};
fetch(GRAPHQL_URL, GRAPHQL_BODY)
.then(res => res.json())
.then(console.log)
.catch(console.error);
</script>
But If I try to execute same piece of code from JavaScript program, I get 404({errors: "Not Found"})
const GRAPHQL_URL = `https://<my-proxy>.herokuapp.com/https://<my-store>/admin/variants/search.json?query=sku:big-mug-black`;
const STOREFRONT_ACCESS_TOKEN = '<my-token>';
const GRAPHQL_BODY = {
'method': 'GET',
'headers': {
'Authorization': `Basic ${btoa('<my-api-key>' + ':' + '<my-password>')}`,
'X-Shopify-Storefront-Access-Token': STOREFRONT_ACCESS_TOKEN,
'Content-Type': 'application/json',
},
};
fetch(GRAPHQL_URL, GRAPHQL_BODY)
.then(res => res.json())
.then(console.log)
.catch(console.error);
Note: I can fetch all products using same program, so its not an permission issue. Is there something I need to add/remove to achieve same result in my local JavaScript program? Thank you.

I might have solution just for your need.
You gonna need following accesses:
read_products - for finding product
read_script_tags - for read existing script tags
write_script_tags - for writing new script tag
First on your application create console action that will be run automatically every X hours. That action shall download all existing products. You gonna need save them to local database.
Things to remember during downloading all products.
Api limits watch for them in headers (X_SHOPIFY_SHOP_API_CALL_LIMIT)
Pagination, there is variable link in headers (Api pagination)
When you have stored all products locally, you can create search method for it using SKU.
When you have working method you can create find method in your controller. You will use this method to create request from shopify page to your server.
After creating this you can add JS by yourself or even better automate it with script tags.
ScriptTag on Shopify are basically javascript code that you host on your side and they are automatically loaded TO EACH page in shopify (except basket).
POST /admin/api/2021-01/script_tags.json
{
"script_tag": {
"event": "onload",
"src": "https://djavaskripped.org/fancy.js"
}
}
With that you can add javascript that can be used to create find request on your website. And you can return result back.
I created simplified graph for that.
.

Related

How to use a private API key with Nuxt (on the client)?

Problem Solved
If you're struggling with the same issue, look at the accepted answer which is one way to achieve it by using serverMiddleware
I'm using an API which required a private key. I've stored the key inside a .env file, and called it in the nuxt configuration file, like this :
privateRuntimeConfig: {
secretKey: process.env.MY_SECRET_KEY
},
My API call is done inside the asyncData() hook on my index page. It works fine when i load this page, or reload it, but everytime i use the navigation to come back to this page, i end up with an error (I use a buffer to convert my API key to base64)
First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.
After some research and debugging, i found out that my private key wasn't available at the time, and the "secret" value used in my api call was "undefined".
The thing I don't get is why is this working on initial load / reload but not on page navigation ? And is there a way to fix it without using a backend ? (SSR for SEO and the ability to use private keys without exposing them are the main reasons why i used Nuxt for my project)
Here is my code :
async asyncData({ $content, store, $config }) {
const secret = Buffer.from($config.secretKey).toString('base64')
const request = await fetch('https://app.snipcart.com/api/products', {
headers: {
'Authorization': `Basic ${secret}`,
'Accept': 'application/json'
}
})
const result = await request.json()
store.commit('products/addProducts', result)
const stocks = store.getters['products/getProducts']
return { stocks }
},
Update
Looking at the #nuxtjs/snipcart module's key key and since it's a buildModules, you can totally put it there since it will be available only during the build (on Node.js only)!
For more info, Snipcart do have a lot of blog posts, this one based on Nuxt may help clearing things up: https://www.storyblok.com/tp/how-to-build-a-shop-with-nuxt-storyblok-and-snipcart
You do have your key initially because you're reaching the server when you enter the page or hard refresh it.
If you navigate after the hydration, it will be a client side navigation so you will not be able to have access to the private key. At the end, if your key is really private (nowadays, some API provide keys that can be exposed), you'll need to work around it in some ways.
Looking at Snipcart: https://docs.snipcart.com/v3/api-reference/authentication, it clearly states that the key should be available in
Appear in your compiled front-end assets (HTML, JavaScript)
Meanwhile, if you need to make another call to your backend (trying to access something else than products), you'll need to make a second call.
With Nuxt2, you cannot reach for the backend each time as of right now since you will stay in an SPA context (Nuxt is a server then client Vue app basically). But you could write down the token into a cookie or even better, use a backend as a proxy to hide this specific key (or even a serverless function).
Some more info can be found on my other answer here: https://stackoverflow.com/a/69575243/8816585
Thanks #kissu for your (very) quick answer :)
So, based on what you said and your other answer on the subject, i've made a server Middleware in Nuxt in my server folder.
server/snipcart.js
const bodyParser = require('body-parser')
const axios = require('axios')
const app = require('express')()
app.use(bodyParser.json())
app.all('/getProducts', (request, response) => {
const url = 'https://app.snipcart.com/api/products'
const secret = Buffer.from(process.env.SNIPCART_SECRET).toString('base64')
const config = {
headers: {
'Authorization': `Basic ${secret}`,
'Accept': 'application/json'
}
}
axios
.get(url, config)
.then(res => {
const products = {}
res.data.items.forEach(
item => {
const productId = item.userDefinedId.replace(/-/g, '')
const stocks = {}
item.variants.forEach(
variant => {
const size = variant.variation[0].option
const stock = variant.stock
stocks[size] = stock
}
)
products[productId] = stocks
}
)
response.json(products)
})
.catch( err => response.json(err) )
})
module.exports = app
Correct me if i'm wrong, but I think that's basically the same as using a server as a proxy right ? Based on Nuxt lifecycle hooks, the serverMiddleware one is only run on the server, so my API key shouldn't be exposed to the client ? (I still need to do some refactoring to clean the code, but at least it's working) (https://nuxtjs.org/docs/concepts/nuxt-lifecycle/#server & https://nuxtjs.org/docs/configuration-glossary/configuration-servermiddleware/)
nuxt.config.js
serverMiddleware: [
{ path: "/server", handler: "~/server/snipcart.js" }
]
index.vue (where my snipcart API call was previously made, i guess now I should move this call directly from the product card component where the data is needed) :
async asyncData({ $content, store, $axios }) {
await $axios
.get('/server/getProducts')
.then(res => store.commit('products/addProducts', res.data))
.catch(err => console.log(err))
const stocks = store.getters['products/getProducts']
return {stocks, masterplanProducts }
},
PS : Snipcart does provide a public API key, but the use is very limited. In order to access the remaining stock for each product, i have to use the private key (which allows for some other operations, like removing products / accessing orders and such)
UPDATE :
It's not working when the website is fists accessed from any other page than the one one where the API call is, since the store won't have any data from the API call)
Okay, now I feel dumb. I found a way to make it work. I guess taking the time to explain my problem helped me understand how to solve it.
For those who encounter a similar issue, i fixed it by wrapping my API call with a If statement.
if ($config.secretKey) {
const secret = Buffer.from($config.secretKey).toString('base64')
const request = await fetch('https://app.snipcart.com/api/products', {
headers: {
'Authorization': `Basic ${secret}`,
'Accept': 'application/json'
}
})
const result = await request.json()
store.commit('products/addProducts', result)
}
const stocks = store.getters['products/getProducts']
This way, i can just skip the API call and access values from my vuex store.

Why isn't express's res.render called inside the PUT method?

I have an express app with ejs view engine.
In the ejs template, I have a button with an onClick event to trigger a fetch request:
<button class="remove-btn" onclick="removeGroceryItem('<%= index %>')" >Remove</button>
the fetch function
function removeGroceryItem (id) {
fetch('/', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ groceryItem: id })
})
.then((value)=> console.log(value))
.catch((e)=>console.error(e))
}
The express app route handler:
/* PUT grocery list*/
app.put('/', function(req, res, next) {
groceryItemsContainer.splice(Number.parseInt(req.body.groceryItem), 1)
res.render('index', {
title: 'Express Grocery List App',
notification: 'removed',
groceryList: groceryItemsContainer
})
res.end();
})
The PUT request is received with a status of 200. But the res.render does not seem to be triggered -- why is that??
It appears from your comment that you are expecting the result of your res.render() to display in the browser. That is NOT what an Ajax call using fetch() does from your browser Javascript. It ONLY gets the content back to your Javascript. It does not display anything in the browser.
So, the content you res.render() goes back to your Javascript in your web page and the console.log(value) you have in your Javascript in the web page should actually be showing the content in the console from the res.render(). If you want that content to display in the browser, then you need to insert that content into the current browser DOM yourself with your own Javascript.

Axios network error even though the post request returns 200

After enabling CORS and everything on my server, the error persists.
In other forms inside my app, uploading pictures works... but in this exact form, on iPhone it works absolutely fine, but on android after submitting, all I get is a "network error" although the post returns 200. I think this is an axios problem. Only on android I get this issue.
my code is the following:
const data = new FormData()
data.append('subject_id', this.props.navigation.getParam('id'))
data.append('name', this.state.title)
data.append('progress', this.state.progress * 100)
data.append('description', this.state.description)
data.append('date', this.state.date)
data.append('image', {
uri: this.state.image,
type: 'image/jpeg',
name: 'image'
});
axios.post('https://example.com/api/auth/createTask', data, {
headers: {
'Authorization': access,
"Content-Type": "multipart/form-data"
},
}).then(res => {
this.props.navigation.navigate('ViewHW', { id: res.data.id })
}).catch(res => {
console.log(res)
})
I would really appreciate the help on this one.
I doubt that it's an axios issue.
If you're using an image picker or camera make sure you check the documentation as the path to the file selected differs between android and iOS.
Make sure you change the path of the item based on platform.OS === 'android'.
It should be clearly described in the docs of whatever you're using.
This ended up being a problem with react-native. The bug is now patched (the new version 0.63.3)

How to render HTML to the user of a step function endpoint?

I'm using serverless and https://github.com/horike37/serverless-step-functions to try and implement a system that is hit by a user, returns HTML based on a database entry for the params provided and then moves to a second function that writes to the database (without forcing the user to wait).
I think a step function in the right approach but I can't seem to get it to return HTML - it always returns a JSON body with the executionArn and startDate. e.g.
{
"executionArn": "arn:aws:states:us-west-2:.......etc...",
"startDate": 1513831673.779
}
Is it possible to have my html body return? At the moment my lambda function returns a simple h1 tag:
'use strict';
module.exports.requestHandler = (event, context, callback) => {
const response = {
statusCode: 200,
headers: {
'Content-Type': 'text/html'
},
body: `<h1>Success!</h1>`,
};
callback(null, response);
};
This is the state machine I'm aiming to create.
I would suggest going for a react/angular/vue frontend hosted e.g. on S3/CDN that uses serverless for backend queries only, instead of rendering dynamic HTML through Lambdas. The 'standard' approach allows you to build apps that are much more responsive and can benefit from e.g. CDNs.
See e.g. https://www.slideshare.net/mitocgroup/serverless-microservices-real-life-story-of-a-web-app-that-uses-angularjs-aws-lambda-and-more or https://serverless-stack.com/

react native fetch not sending body content

I am new to fetch in react native and followed this tutorial.
I am trying to send a json body to an private api server, I checked the server log and found out that the body content is empty.
Here is the code in react native
authenticateLogIn(){
fetch('<URL>', {
method: 'POST',
header: {'Content-Type': 'application/json', 'Accept': 'application/json'},
body: JSON.stringify({'username': '<username>', 'password':'<password>'})
})
.then((incoming) => incoming.json())
.then((response) => {
console.log(response.header);
Alert.alert(JSON.stringify(response.body));
})
.done();
Maybe because it shoueld be headers? (with the s)
It might be because your JSON body isn't being parsed correctly. bodyParser https://github.com/expressjs/body-parser is part of the middleware that parses request bodies and needs to be configured.
req.body kept coming back empty for me until I added app.use(bodyParser.json() into server.js file. Make sure you import body-parser.