I've been trying to figure this out but still having trouble. I have an array of objects and what I am trying to do is update the key of slug to ID and change it's value to the ID of the blog post that is returned from an API. I also want to do the same for the key featuredImage where I return data from API and then update the value in this array of objects. I don't have anyway to associate the slug to the featuredImage except for the way they appears in the original postData array of objects. If I am going to be making API calls and also manipulating the data is there a way to ensure that I don't mess up my key / value pairs here. What I've been struggling with is I can't seem to hold the order properly and my data ends up being in the wrong order in my final array of objects. Any help would be much appreciated.
const postData =[
{ slug: sample-post-1, featuredImage: 'https://www.rusticfurnitureboston.com/hubfs/Blog_Media/rustic20coffee20table.jpeg%3Ft=1528912781831-6.jpeg' },
{ slug: sample-post-2, featuredImage: 'https://www.rusticfurnitureboston.com/hubfs/Blog_Media/Amazing-Table-For-Flamboyant-Furniture-Home-Design-Ideas-With-Rustic-Furniture-Coffee-Table.jpg%3Ft=1528912781831-6.jpeg' },
{ slug: sample-post-3, featuredImage: 'https://www.rusticfurnitureboston.com/hubfs/Blog_Media/envoy-lookout-rooftop-11b-780x520.jpg%3Ft=1528912781831-6.jpeg' },
{ slug: sample-post-4, featuredImage: 'https://www.rusticfurnitureboston.com/hubfs/Blog_Media/mountain-landscape-wallpaper-29048-29765-hd-wallpapers.jpg%3Ft=1528912781831-6.jpeg'},
{ slug: sample-post-5, featuredImage: 'https://www.rusticfurnitureboston.com/hubfs/Blog_Media/mountain-landscape-wallpaper-29048-29765-hd-wallpapers.jpg%3Ft=1528912781831-6.jpeg' }
]
This is the function that I am trying to run but this is where I try to create a new Object based on the response the items are out of order.
const uploadImages = async (postData) => {
const newImages = [];
try {
await Promise.all(postData.map(async featuredImage => {
const option = {
method: 'POST',
url: fileAPIURL,
qs: {
access_token: ACCESS_TOKEN,
},
headers: {
'content-type': 'multipart/form-data'
},
formData: {
folder_paths: 'Blog_Media',
image: request(featuredImage)
},
json: true
}
return request(option)
.then((response) => {
response.objects.map((content) => {
return newImages.push(Object.assign({
featuredImage:content.url }));
});
})
}))
return newImages;
} catch (error) {
throw new Error('Cannot upload image to file manager, try checking
your URL');
}
}
When you run a bunch of asynchronous operations in parallel, they can complete it any order. So, when you do this:
newImages.push(...)
The newImages array is going to be in the order that your parallel async operations were completed. That will be some random order.
But, Promise.all() will keep the data in the proper order if you let it manage the returned data for you. So, inside of postData.map(), you can return an array of objects. Then Promise.all() will keep that array in the proper order relative to the other arrays. Then, at the end of the Promise.all().then(), you will have an array of arrays (all in the proper order) so all you need to do to get it into a single flat array in the proper order is to flatten that.
const uploadImages = (postData) => {
return Promise.all(postData.map(featuredImage => {
const option = {
method: 'POST',
url: fileAPIURL,
qs: {
access_token: ACCESS_TOKEN,
},
headers: {
'content-type': 'multipart/form-data'
},
formData: {
folder_paths: 'Blog_Media',
image: request(featuredImage)
},
json: true
}
return request(option).then((response) => {
return response.objects.map((content) => {
return {featuredImage: content.url};
});
});
})).then(results => {
// flatten the array of arrays into a single array
return [].concat.apply([], results);
}).catch(error => {
throw new Error('Cannot upload image to file manager, try checking your URL ');
});
}
Also, you don't seem to be actually using async/await for anything useful here to I removed that.
The one part of your code I don't understand is where you pass:
image: request(featuredImage)
as part of the formData to another request. That will put a promise into the formData since request(featuredImage) will return a promise. Is that really what you want?
Related
I'm working on a project that utilizes the IGDB to display information about various video game titles. I've got an API call on the search page that returns the information appropriately. Upon clicking on one of those titles from the search, I load a new page with detailed information about the game. The API call for the detailed information is the exact same as the one for the search functionality (exception being that they retrieve different fields from the DB, aside from that, exactly the same) and both return the data appropriately and in the exact same format (an array of objects.) However, I have had a lot of trouble displaying the data on the game details page.
Here is the response that I am having trouble displaying:
And, just for the sake of as much detail for you guys as possible, here is the successfully displayed response from the search page:
The way I have it set up is to run a useEffect hook on page load. The hook does run, and the data is returned, but displaying the data has been a challenge. Here is the current hook.
useEffect(() => {
async function getGameId(gameId) {
const response = await getSpecificGame(gameId);
if (!response.ok) {
throw new Error('Something went wrong...');
}
const result = response.json();
const gameData = result?.map((game) => ({
gameId: game.id,
name: game.name,
cover: game.cover,
summary: game.summary,
platforms: game.platforms,
genres: game.genres,
}));
setSelectedGame(gameData);
}
getGameId(gameId);
}, [])
With this code I receive the following error:
Uncaught (in promise) TypeError: result.map is not a function
With the error being with result.map, I'm very lost on where to go from here. I have wondered if perhaps the response.json() line is unnecessary or misplaced, but with the data returning in the exact same fashion as it does for the search page, I'm not sure what I would need to change. The error is not thrown on the response, meaning the data comes back ok, but it is thrown on the result.map() line, meaning either the result = response.json() is somehow incorrect/unnecessary, or I'm missing another line that needs to be there. This confuses me greatly though, since the other API call I make to perform the search works and is set up the same way. For extra context, I will post the properly functioning API call from the search page as well, this one is within a form handler:
const handleFormSubmit = async (event) => {
event.preventDefault();
if (!searchInput) {
return false;
}
try {
const response = await getGame(searchInput);
if (!response.ok) {
throw new Error('Something went wrong...');
}
const result = await response.json();
const gameData = result.map((game) => ({
gameId: game.id,
name: game.name,
cover: game.cover,
}));
setSearchedGames(gameData);
setSearchInput('');
} catch (err) {
console.error(err);
}
};
Here are the API calls that go with those functions.
export const getGame = (searchInput) => {
return fetch(`https://id.twitch.tv/oauth2/token?client_id=************&client_secret=****************&grant_type=client_credentials`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
const accessToken = data.access_token;
return fetch(`https://fathomless-river-46653.herokuapp.com/https://api.igdb.com/v4/games/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Client-ID': '***********',
'Authorization': `Bearer ${accessToken}`
},
body: `
search "${searchInput}";
fields name,cover.url;`
})
});
};
export const getSpecificGame = (gameId) => {
return fetch(`https://id.twitch.tv/oauth2/token?client_id=************&client_secret=**************&grant_type=client_credentials`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
const accessToken = data.access_token;
return fetch(`https://fathomless-river-46653.herokuapp.com/https://api.igdb.com/v4/games/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Client-ID': '**************',
'Authorization': `Bearer ${accessToken}`
},
body: `
fields name,summary,platforms.name,genres.name,cover.url;
where id = ${gameId};`
})
});
}
This seems to be a simple matter of appropriately changing the syntax to function with useEffect instead of a form handler, but it is proving to be quite the challenge. I've looked over probably 20 threads of similar issues, but none of which have been able to solve my problem.
I've wondered if I should go another route entirely, completely removing the useEffect hook and trying something else, but this feels like a learning opportunity on how to make use of useEffect so I'm trying my best to get this working.
I would be happy to attach any extra code to assist with an answer. I posted a very similar question yesterday and received some helpful responses, but since then have made progress and reached another roadblock point. Thank you in advance to anyone who is able to assist!
const result = response.json();
There is a small error here, you need to await it:
const result = await response.json();
In the other example you posted from a form handler, it's awaited correctly, so it works.
I'm trying to display results from an API but it seems like an entire section of the page isn't being returned. There are no errors present so solving this has been somewhat confusing.
My assumption is that based on the way the API has returned the data, my current setup for destructuring the data is incorrect. Here is the response I receive in the console from the API.
I want to apend these results to a page to display the name and cover of these video game titles.
Section displaying results
<Container>
<h2>
{searchedGames?.length
? `Viewing ${searchedGames.length} results:`
: 'Search for a game to begin'}
</h2>
<CardColumns>
{(searchedGames || []).map((game) => {
return (
<Card key={game.gameId} border='dark'>
{game.cover ? (
<Card.Img src={game.cover} alt={`The cover for ${game.name}`} variant='top' />
) : null}
<Card.Body>
<Card.Title>{game.name}</Card.Title>
</Card.Body>
</Card>
);
})}
</CardColumns>
</Container>
Form Handler
const handleFormSubmit = async (event) => {
event.preventDefault();
if (!searchInput) {
return false;
}
try {
const response = await getGame(searchInput);
if (!response.ok) {
throw new Error('Something went wrong...');
}
const { result } = await response.json();
const gameData = result?.map((game) => ({
gameId: game.id,
name: game.name,
cover: game.cover.url,
}));
setSearchedGames(gameData);
setSearchInput('');
} catch (err) {
console.error(err);
}
};
Upon searching, I receive the API response with the appropriate data, but the page does not display anything at all (not even the "Viewing xxx number of results" section, which I feel is a clue pointing to the searchedGames array.) I'd be happy to add any extra information or code if someone would like. I feel like there is a simple solution but I have been unable to find the method to fix this. Thank you in advance for any assistance anyone can offer!
EDIT:
There was a helpful response here recommending I check my searchedGames array and, once I did, I saw that it was empty. So the data from the API is not getting put into the "searchedGames" array. I am researching how to fix this, but if anyone responds to the question I feel this may be a helpful detail to assist. The problem is that my searchedGames array is not being filled with the API data.
EDIT 2:
export const getGame = (searchInput) => {
return fetch(`***********`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
const accessToken = data.access_token;
return fetch(`https://fathomless-river-46653.herokuapp.com/https://api.igdb.com/v4/games/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Client-ID': '********',
'Authorization': `Bearer ${accessToken}`
},
body: `
search "${searchInput}";
fields name,cover.url;`
})
});
};
Not entirely sure if there are details missing on the actual API response data format but I'll try giving an answer.
The json() method of fetch returns the response body of your HTTP request as JSON. So with your response data being the one of your first screenshot, it's an array of objects but not an object with an result attribute. However, latter is what you assume in your code:
const { result } = await response.json();
Then you proceed as if result was the response array of your first screenshot while it actually is undefined, so that gameData resolves to undefined after optional chaining.
const gameData = result?.map((game) => ({
gameId: game.id,
name: game.name,
cover: game.cover.url,
}));
Try out using the JSON response directly without destructuring. This should give you the actual array of game objects:
const result = await response.json();
The problem is that I can console.log the array from the useEffect hook, but when I go to access it, it returns an empty array.
I am trying to upload data to my Algolia index.
Here is the current useEffect:
useEffect(() => {
const fetchListings = () => {
api
.listNasdaq()
.then((response) => {
setListings(response.data);
console.log(response); //I can see the array of data here.
})
.catch((error) => {
console.log(error);
});
};
fetchListings();
}, []);
api.js:
listNasdaq: () =>
exchangeApi({
method: "GET",
url: "/companies/list-by-exchange",
transformResponse: [
function (data) {
// Do whatever you want to transform the data
console.log("Transforming Nasdaq Data...");
const json = JSON.parse(data);
const stocks = Object.keys(json["results"]);
const stockNames = stocks.map(
(stock) =>
(stock = {
stock,
stockName: String(json["results"][stock]["companyName"]),
symbol: String(json["results"][stock]["symbol"]),
industry: String(json["results"][stock]["industryOrCategory"]),
})
);
data = {
stockNames,
};
return data;
},
],
}),
Algolia:
nasdaqIndex
.saveObjects(listings, {
autoGenerateObjectIDIfNotExist: true,
})
.then(({ objectIDs }) => {
console.log("algolia stuff", objectIDs);
});
I'm a little confused by your map function. Are you injecting the full stock object back into itself ((stock = { stock, ...)? What does the final stock object look like when you log it? I'm worried that might be confusing Algolia when you go to save the record.
const stockNames = stocks.map(
(stock) =>
(stock = {
stock,
stockName: String(json["results"][stock]["companyName"]),
symbol: String(json["results"][stock]["symbol"]),
industry: String(json["results"][stock]["industryOrCategory"]),
})
);
Also, I assume the call to nasdaqIndex.saveObjects() occurs in setListings()? It looks like some of this code may be missing here.
My page is consuming data from an API/JSON and passes two parameters via the API URL:
The code:
fetchSegments(building, reselect) {
const bearerToken = this.getToken();
const simulations = filter(simulationJson, { clientName: building.clientName}/ ${building.siteName} )
//fetch(`theapiurl.net/api/simulations/${building.clientName}/${building.siteName}`, {
// method: 'GET',
// headers: {
// 'Content-type': 'application/json',
// 'Authorization': `Bearer ${bearerToken}`
// },
// })
// .then(response => {
// if(!response.ok && response.status === 404) {
// this.setState({ segments: [], selected: undefined })
// return Promise.reject(response);
// }
// return response.json();
// })
// .then(simulation => {
// let selectIndex = 0;
// if(reselect) {
// selectIndex = this.state.selected;
// }
this.setState({ segments: simulation.segments, selected: selectIndex })
this.props.onSelect(simulation.segments[selectIndex], reselect)
// })
}
I currently have to consume the JSON file locally and so I commented out the bits of code in the above snippet, but I get a syntax error when trying to append the second parameter:
const simulations = filter(simulationJson, { clientName: building.clientName}/ **${building.siteName**} )
I was able to fix the syntax issue in another file that uses only one parameter, but having issues when it comes to passing two. ...could I get some help with this please
If you are sure this objects have values, you can try an alternate method,
fetch('theapiurl.net/api/simulations/'+building.clientName+'/'+building.siteName)
I'm using jest+nock+jsdom modules to test my React\Redux application.
I need to test this async action function:
export function updateUserPhoto (file, token) {
const data = new FormData()
data.append('file', file)
return dispatch => {
dispatch(userPutPhotoRequest())
return axios({
method: 'PUT',
headers: {
'x-access-token': token
},
data: data,
url: API_URL + '/user/photo'
})
.then(res => dispatch(userPutPhotoSuccess(res.data)))
.catch(err => dispatch(userPutPhotoFilure(err)))
}
}
So i'm using jsdom to provide FormData and File objects into tests:
const {JSDOM} = require('jsdom')
const jsdom = (new JSDOM(''))
global.window = jsdom.window
global.document = jsdom.window.document
global.FormData = jsdom.window.FormData
const File = jsdom.window.File
global.File = jsdom.window.File
And this is the method to test "upload photo" function:
it('creates USER_UPDATE_SUCCESS when updating user photo has been done', () => {
const store = mockStore(Map())
const file = new File([''], 'filename.txt', {
type: 'text/plain',
lastModified: new Date()
})
const expectedFormData = new FormData()
expectedFormData.append('file', file)
nock(API_URL, {
reqheaders: {
'x-access-token': token
}
}).put('/user/photo', expectedFormData)
.reply(200, {body: {}})
const expectedActions = [
{
type: ActionTypes.USER_PUT_PHOTO_REQUEST
},
{
type: ActionTypes.USER_PUT_PHOTO_SUCCESS,
response: {
body: {}
}
}
]
return store.dispatch(actions.updateUserPhoto(file, token))
.then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions)
})
})
Where i'm using nock to mock axios requests, redux-mock-store to mock Redux store.
Creating File and FormData objects to compare it with response from axios.
And then i'm calling action function passing file and token as parameters.
In production action function works and dispatch action success fine. But in testing i'm receiving error:
Error: Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream
When i pass into axios empty object as data test passes, so problem in FormData object.
How can i mock FormData object for axios in an appropriate way to make this test work ?
This answer is coming way too late, but I was looking to do something similar and I wanted to post a solution here that someone else might stumble across and find useful.
The main problem here is that nock mocks network requests and not Javascript libraries. FormData is a Javascript object that eventually gets transformed to text when making network requests. By the time the FormData object makes it to nock, its been converted to a string or a Buffer, hence the error you see. nock is unable to use the FormData object for comparison.
You have a few options:
1. Easiest solution
Just don't match against the data in the PUT request. The reason you are mocking is because you don't want a real HTTP request to go out but you want a fake response back. nock only mocks the request once, so if you mock all PUT requests to /user/photo nock will catch it but only for that test:
nock(API_URL, {
reqheaders: {
'x-access-token': token
}
}).put('/user/photo')
.reply(200, {body: {}})
Before you implement the test this way, think about what your test is trying to verify. Are you trying to verify that the file is sent in the HTTP request? If yes, then this is a poor option. Your code could send a completely different file than the one dispatched and still pass this test. If however you have another test to verify the file is being put in the HTTP request properly then this solution might save you some time.
2. Easy solution for getting nock to fail on not matching the request
If you do want the test to fail if your code passed a corrupted or wrong file, then he simplest solution would be to test for the filename. Since your file is empty there is no need to match the content, but we can match on the filename:
nock(API_URL, {
reqheaders: {
'x-access-token': token
}
}).put('/user/photo', /Content-Disposition\s*:\s*form-data\s*;\s*name="file"\s*;\s*filename="filename.txt"/i)
.reply(200, {body: {}})
This should match the simple case where you have one file uploading.
3. Matching the content of form data fields
Say you have additional fields to be added to your request
export function updateUserPhoto (file, tags, token) {
const data = new FormData()
data.append('file', file)
data.append('tags', tags)
...
OR you have fake content in the file that you want to match on
const file = new File(Array.from('file contents'), 'filename.txt', {
type: 'text/plain',
lastModified: new Date()
})
This is where things get a bit complex. Essentially what you need to do is to parse the form data text back into an object and then write your own matching logic.
parse-multipart-data is a fairly simple parser that you could use:
https://www.npmjs.com/package/parse-multipart-data
Using that package your test might look something like this
it('creates USER_UPDATE_SUCCESS when updating user photo has been done', () => {
const store = mockStore(Map())
const file = new File(Array.from('file content'), 'filename.txt', {
type: 'text/plain',
lastModified: new Date()
})
nock(API_URL, {
reqheaders: {
'x-access-token': token
}
}).put('/user/photo', function (body) { /* You cannot use a fat-arrow function since we need to access the request headers */
// Multipart Data has a 'boundary' that works as a delimiter.
// You need to extract that
const boundary = this.headers['content-disposition']
.match(/boundary="([^"]+)"/)[1];
const parts = multipart.Parse(Buffer.from(body),boundary);
// return true to indicate a match
return parts[0].filename === 'filename.txt'
&& parts[0].type === 'text/plain'
&& parts[0].data.toString('utf8') === 'file contents'
&& parts[1].name === 'tags[]'
&& parts[1].data.toString('utf8') === 'tag1'
&& parts[2].name === 'tags[]'
&& parts[2].data.toString('utf8') === 'tag2';
})
.reply(200, {body: {}})
const expectedActions = [
{
type: ActionTypes.USER_PUT_PHOTO_REQUEST
},
{
type: ActionTypes.USER_PUT_PHOTO_SUCCESS,
response: {
body: {}
}
}
]
return store.dispatch(actions.updateUserPhoto(file, ['tag1', 'tag2'], token))
.then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions)
})
})
I was dealing with the same issue, the problem was that axios was setting http as the default adapter. And xhr is the one you need.
// axios/lib/defaults.js
function getDefaultAdapter() {
var adapter;
// Only Node.JS has a process variable that is of [[Class]] process
if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// For node use HTTP adapter
adapter = require('./adapters/http');
} else if (typeof XMLHttpRequest !== 'undefined') {
// For browsers use XHR adapter
adapter = require('./adapters/xhr');
}
return adapter;
}
So, setting the xhr adapter explicitly on the axios calls worked for me.
Something like:
export function updateUserPhoto (file, token) {
const data = new FormData()
data.append('file', file)
return dispatch => {
dispatch(userPutPhotoRequest())
return axios({
method: 'PUT',
headers: {
'x-access-token': token
},
adapter: require('axios/lib/adapters/xhr'),
data: data,
url: API_URL + '/user/photo'
})
.then(res => dispatch(userPutPhotoSuccess(res.data)))
.catch(err => dispatch(userPutPhotoFilure(err)))
}
}
Also, I had issues with nock and CORS, so, if you have same issue, you can add access-control-allow-origin header
nock(API_URL, {
reqheaders: {
'x-access-token': token
}
})
.defaultReplyHeaders({ 'access-control-allow-origin': '*' })
.put('/user/photo', expectedFormData)
.reply(200, {body: {}})