Upload images with apollo-upload-client in React Native - react-native

I'm trying out Prisma and React Native right now. Currently I'm trying to upload images to my db with the package _apollo-upload-client (https://github.com/jaydenseric/apollo-upload-client). But it's not going so well.
Currently I can select an image with the ImagePicker from Expo. And then I'm trying to do my mutation with the Apollo Client:
await this.props.mutate({
variables: {
name,
description,
price,
image,
},
});
But I get the following error:
Network error: JSON Parse error: Unexpected identifier "POST"
- node_modules/apollo-client/bundle.umd.js:76:32 in ApolloError
- node_modules/apollo-client/bundle.umd.js:797:43 in error
And I believe it's from these lines of code:
const image = new ReactNativeFile({
uri: imageUrl,
type: 'image/png',
name: 'i-am-a-name',
});
Which is almost identical from the their example, https://github.com/jaydenseric/apollo-upload-client#react-native.
imageUrl is from my state. And when I console.log image I get the following:
ReactNativeFile {
"name": "i-am-a-name",
"type": "image/png",
"uri": "file:///Users/martinnord/Library/Developer/CoreSimulator/Devices/4C297288-A876-4159-9CD7-41D75303D07F/data/Containers/Data/Application/8E899238-DE52-47BF-99E2-583717740E40/Library/Caches/ExponentExperienceData/%2540anonymous%252Fecommerce-app-e5eacce4-b22c-4ab9-9151-55cd82ba58bf/ImagePicker/771798A4-84F1-4130-AB37-9F382546AE47.png",
}
So something is popping out. But I can't get any further and I'm hoping I could get some tips from someone.
I also didn't include any code from the backend since I believe the problem lays on the frontend. But if anyone would like to take a look at the backend I can update the question, or you could take a look here: https://github.com/Martinnord/Ecommerce-server/tree/image_uploads.
Thanks a lot for reading! Cheers.
Update
After someone asked after the logic in the server I have decided to past it below:
Product.ts
// import shortid from 'shortid'
import { createWriteStream } from 'fs'
import { getUserId, Context } from '../../utils'
const storeUpload = async ({ stream, filename }): Promise<any> => {
// const path = `images/${shortid.generate()}`
const path = `images/test`
return new Promise((resolve, reject) =>
stream
.pipe(createWriteStream(path))
.on('finish', () => resolve({ path }))
.on('error', reject),
)
}
const processUpload = async upload => {
const { stream, filename, mimetype, encoding } = await upload
const { path } = await storeUpload({ stream, filename })
return path
}
export const product = {
async createProduct(parent, { name, description, price, image }, ctx: Context, info) {
// const userId = getUserId(ctx)
const userId = 1;
console.log(image);
const imageUrl = await processUpload(image);
console.log(imageUrl);
return ctx.db.mutation.createProduct(
{
data: {
name,
description,
price,
imageUrl,
seller: {
connect: { id: userId },
},
},
},
info
)
},
}

Solution has been found.
I am a little embarrassed that this was the problem that I faced and I don't know if I should even accept this answer because of awkward I felt when I fixed the issue. But....
There was nothing wrong with my code, but there was a problem with the dependencies versions. I tried to backtrack everything on my app, so I decided to start from the beginning and create a new account. I expected it to work just fine, but I got this error:
Error: Cannot use GraphQLNonNull "User!" from another module or realm.
Ensure that there is only one instance of "graphql" in the node_modules
directory. If different versions of "graphql" are the dependencies of other
relied on modules, use "resolutions" to ensure only one version is installed.
https://yarnpkg.com/en/docs/selective-version-resolutions
Duplicate "graphql" modules cannot be used at the same time since different
versions may have different capabilities and behavior. The data from one
version used in the function from another could produce confusing and
spurious results.
Then I understand that something (that I didn't think of) was wrong. I checked my dependencies versions and compared them with Graphcool's example, https://github.com/graphcool/graphql-server-example/blob/master/package.json. And I noticed that my dependencies was outdated. So I upgraded them and everything worked! So that was what I had to do. Update my dependencies.
Moral of the story
Always, always check your damn dependencies versions...

Crawling through your code, I have found this repository, which must be the front-end code if I am not mistaken?
As you've mentioned, apollo-upload-server requires some additional set-up and same goes for the front-end part of your project. You can find more about it here.
As far as I know, the problematic part of your code must be the initialisation of the Apollo Client. From my observation, you've put everything Apollo requires inside of src/index folder, but haven't included Apollo Upload Client itself.
I have created a gist from one of my projects which initialises Apollo Upload Client alongside some other things, but I think you'll find yourself out.
https://gist.github.com/maticzav/86892448682f40e0bc9fc4d4a3acd93a
Hope this helps you! 🙂

Related

Mongoose creates empty document with next.js

I'm struggling with what should be a simple case, but I can't seem to get to the bottom of it: I am building a very simple CRUD app with nextjs using MongoDB.
I am following the official nextjs "with-mongodb-mongoose" as a guide (https://github.com/vercel/next.js/tree/canary/examples/with-mongodb-mongoose)
While all should be simple, my code fails to properly create a document with the form data to be sent to MongoDB. In other words, all my entries in MongoDB are empty documents:
{ _id: new ObjectId("63a5aa1317a618f4d3eefab8"), __v: 0 }
Taking it to step by step:
I have created a very simple model that only takes "name" as a paremeter:
import mongoose from 'mongoose'
const raceresultSchema = new mongoose.Schema({
name: String,
});
const Raceresult = mongoose.models.Raceresult || mongoose.model('Raceresult', raceresultSchema)
export default Raceresult
My POSt API route is exactly the same as the one in the GitHub repo from nextjs:
case 'POST':
try {
const newResultDoc = await Raceresult.create(req.body);
res.status(201).json({ success: true, data: newResultDoc })
} catch (error) {
res.status(400).json({ success: false })
}
break
default:
res.status(400).json({ success: false })
break
}
I use ThunderClient in VSCode to simplify, and I make a POST request to the right API route with the following body:
{
"name": "test"
}
I systematically get the following answer:
{
"success": true,
"data": {
"_id": "63a5b71917a618f4d3eefae3",
"__v": 0
}
}
As I'm following the syntax from the Github repo, I don't see what I'm doing wrong.
The DB connection works fine. These empty documents get saved on my MongoDB, with no problem.
When I add elements manually in MongoDB Atlas, that works fine and the items get saved properly, with no problem.
The problem happens whenever I try to use the API route to save a document. So I guess the problem must come from the API POST code or from the body format. However, I can't figure out what is wrong as I'm following the syntax from the official GitHub repo...
I have seen another StackOverflow post with a similar type of issue, but the solution did not seem applicable to my case.
Appreciate your help!
EDIT: I copy/pastd the code from the nextjs github repo (Pet model, and Pet API route) and made a POST request in Thunder Client to save a pet in my database (which is not the point) and... it worked, the document is saved in my database with the data from the req body... I'll keep trying and update this post if I succeed with my own model
So I seem to have solved it like this:
Connected to my MongoDB database in my terminal and dropped all existing collections
stopped the app and restarted running npm run dev
tried the API calls again and... it now works.
I guess it had something to do with saved Schema Types that were not being validated somewhere... I don't know more, but this now works.

Strapi v4 Extending Server API for Plugins does not work

I am trying to follow the Strapi v4.0.0 guide on https://docs.strapi.io/developer-docs/latest/developer-resources/plugin-api-reference/server.html#entry-file for extending the users-permission plugin to add a custom route/controller, but so far have been unsuccessful. I add the custom files as stated in the docs, but there is no change in the UI.
I managed to get this to work for normal API highlighted in yellow, but was unable to do so for the users-permission plugin
In the previous version 3.6.8 this functionality was allowed through the extensions folder.
Am I missing something from the new guide, I even tried copying the files from node_modules > #strapi > plugin-users-permission and adding a new route and method to the exiting controller file but it still does not reflect the change in the section where we assign different route permission to roles. The user-permission plugin still shows the original routes, with no change.
Thanks,
I ran into this thread while researching pretty much the same issue, and I wanted to share my solution.
First of all, I found this portion of the documentation more useful than the one you referenced: https://docs.strapi.io/developer-docs/latest/development/plugins-extension.html
My goal was the write a new route to validate JWT tokens based on the comment made here: https://github.com/strapi/strapi/issues/3601#issuecomment-510810027 but updated for Strapi v4.
The solution turned out to be simple:
Create a new folder structure: ./src/extensions/user-permissions if it does not exist.
Create a new file ./src/extensions/user-permissions/strapi-server.js if it does not exist.
Add the following to the file:
module.exports = (plugin) => {
plugin.controllers.<controller>['<new method>'] = async (ctx) => {
// custom logic here
}
plugin.routes['content-api'].routes.push({
method: '<method>',
path: '/your/path',
handler: '<controller>.<new method>',
config: {
policies: [],
prefix: '',
},
});
return plugin;
};
If you're unsure what controllers are available, you can always check the API documentation or console.log(plugin) or console.log(plugin.controllers).
After the admin server restarts, you should see your new route under the user-permissions section as you would expect, and you can assign rights to it as you see fit.
My full strapi-server.js file including the logic to validate JWT:
module.exports = (plugin) => {
plugin.controllers.auth['tokenDecrypt'] = async (ctx) => {
// get token from the POST request
const {token} = ctx.request.body;
// check token requirement
if (!token) {
return ctx.badRequest('`token` param is missing')
}
try {
// decrypt the jwt
const obj = await strapi.plugin('users-permissions').service('jwt').verify(token);
// send the decrypted object
return obj;
} catch (err) {
// if the token is not a valid token it will throw and error
return ctx.badRequest(err.toString());
}
}
plugin.routes['content-api'].routes.push({
method: 'POST',
path: '/token/validation',
handler: 'auth.tokenDecrypt',
config: {
policies: [],
prefix: '',
},
});
return plugin;
};
When exporting routes you need to export the type, either content-api or admin. Look at the Strapi email plugin in node_modules for example, change the folder and file structure in your routes folder to match that and then you will be able to set permissions in the admin panel.
If your Strapi server is using Typescript, make sure that you name your extension files accordingly. So instead of strapi-server.js, you would need to name your file strapi-server.ts.

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.

Given oauth_app resource not found - Magic.Link

I tries to integrate magic link social authentication to my React Native Mobile app and it always gives error Given oauth_app resource not found as in the attached file.
I use the code as described in the documentation and they're as follows,
const MagicElement = new Magic('pk_test_***********', {
extensions: [new OAuthExtension()],
});
try {
const result = await MagicElement.oauth.loginWithPopup({
provider: 'apple' /* 'google', 'facebook', 'apple', or 'github' */,
redirectURI: 'testapp://demo/your/oauth/callback',
});
console.log('result', result)
} catch (error) {
console.log('error', error)
}
}
https://docs.magic.link/client-sdk/web/social-login
Appreciate if anyone can help to solve this issue.
Be careful what parameters you are passing to loginWithPopup etc. An easy step to take if you encounter this screen is to look at the network tab filtered on "document" type.
In my case [object%20Object] in the request URL was a giveway:
https://auth.magic.link/v1/oauth2/[object%20Object]/start?magic_api_key=...
After fixing the parameters the URL became something like
https://auth.magic.link/v1/oauth2/google/start?magic_api_key=...
and the error went away.

When switching expo from develop mode to production my RSAA calls become invalid, why?

Background
I am using expo to view my react native app on my android phone. I have implemented a node server that the app retrieves data from. To retrieve data I am using the library redux-api-middleware and storing it in the redux store.
I have used this setup before with regular react apps but not react-native.
Problem
When I switch the app from develop to production mode in expo my calls to the server no longer work. I get the following response instead of the data like in dev mode.
Object {
"error": true,
"payload": [InvalidRSAA: Invalid RSAA],
"type": Symbol {
"_k": "Symbol(REQUEST_TEST_REQ)_l.b74uhq9lvf",
},
}
My Current Call
import { RSAA } from 'redux-api-middleware';
import { NODE_API_URL } from 'react-native-dotenv';
export const REQUEST_TEST_REQ = Symbol('REQUEST_TEST_REQ');
export const REQUEST_TEST_SUC = Symbol('REQUEST_TEST_SUC');
export const REQUEST_TEST_FAIL = Symbol('REQUEST_TEST_FAIL');
const requestSomeDataFromTheServer = () => {
return {
[RSAA]: {
endpoint: `${NODE_API_URL}/api/test`,
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
types: [
REQUEST_TEST_REQ,
REQUEST_TEST_SUC,
REQUEST_TEST_FAIL
]
}
}
};
export { requestSomeDataFromTheServer };
Question
So I follow the rules of the RSAA request but I see this failure as soon as a request is made. It is definitely not my node server since it works in dev mode. Can someone shed some light on this issue for me?
Symbols create invalid requests in production mode
I thought that maybe it was a problem with fetch in production so I added a polyfill first but this didn't help. This gave me the idea to take everything back to basics and so i turned the symbols into strings.
export const REQUEST_TEST_REQ = 'REQUEST_TEST_REQ';
export const REQUEST_TEST_SUC = 'REQUEST_TEST_SUC';
export const REQUEST_TEST_FAIL = 'REQUEST_TEST_FAIL';
By changing these back to plain strings the request is valid and so my problem is solved.
I am still unaware as to why the symbols break in production mode because they work fine in the develop. Also I am using babel polyfill to make them "safe" to use.
If anyone could clear up the rest of this mystery that would be great.