Unable to save data in gatsby graphql layer while creating source plugin - node.js

I am trying to fetch all the videos of a youtube channel grouped by playlist. So first i am fetching all the playlists and then again fetching the corresponding videos.
const fetch = require("node-fetch")
const queryString = require("query-string")
module.exports.sourceNodes = async (
{ actions, createNodeId, createContentDigest },
configOptions
) => {
const { createNode } = actions
// Gatsby adds a configOption that's not needed for this plugin, delete it
delete configOptions.plugins
// plugin code goes here...
console.log("Testing my plugin", configOptions)
// Convert the options object into a query string
const apiOptions = queryString.stringify(configOptions)
const apiUrl = `https://www.googleapis.com/youtube/v3/playlists?${apiOptions}`
// Helper function that processes a content to match Gatsby's node structure
const processContent = content => {
const nodeId = createNodeId(`youtube--${content.id}`)
const nodeContent = JSON.stringify(content)
const nodeData = Object.assign({}, content, {
id: nodeId,
parent: null,
children: [],
internal: {
type: `tubeVideo`,
content: nodeContent,
contentDigest: createContentDigest(content)
}
})
return nodeData
}
return fetch(apiUrl)
.then(res => res.json())
.then(data => {
data.items.forEach(item => {
console.log("item", item.id)
//fetch videos of the playlist
let playlistApiOption = queryString.stringify({
part: "snippet,contentDetails",
key: "AIzaSyDPdlc3ctJ7yodRZE_GfbngNBEYbdcyys8",
playlistId: item.id,
fields: "items(id,snippet(title,description,thumbnails),contentDetails)"
})
let playlistApiUrl = `https://www.googleapis.com/youtube/v3/playlistItems?${playlistApiOption}`
fetch(playlistApiUrl)
.then(res => res.json())
.then(data => {
data.items.forEach(video => {
console.log("videos", video)
// Process the video data to match the structure of a Gatsby node
const nodeData = processContent(video)
// console.log(nodeData)
// Use Gatsby's createNode helper to create a node from the node data
createNode(nodeData)
})
})
})
})
}
Here Nodes are getting created for individual videos. But can't query this nodes from graphql store. ie. datas are not getting saved in graphql store

edit: Wait, I just realize it's inside a loop. Your sourceNodes is not waiting for the fetch inside your loop to resolve. In this case, you'd have to use something like Promise.all to resolve each item in the loop. Code's updated to reflect that.
return fetch(apiUrl)
.then(res => res.json())
.then(data => {
return Promise.all(
data.items.map(item => {
/* etc. */
return fetch(playlistApiUrl)
.then(res => res.json())
.then(data => {
data.items.forEach(video => {
/* etc. */
createNode(nodeData)
})
})
)
})
})
Check out async/await syntax, it might make finding these type of issue easier.

Related

Retrieving Data from Firestore with angular/fire/rxjs

I'm trying to get collection data from a firestore instance and don't want to use valueChanges{idField: id}. So far this is the only solution that somehow processes some of the data and gets the output close to what I need.
I'm new to angular & angular/fire as well as to rxjs and am really struggling to understand observables, pipe, map and rxjs in general.
What am I missing here?
async fetchJobs() {
let jc = await collection(this.firestore, 'jobs');
let cSN = await collectionSnapshots(jc);
let jobsArr = cSN.pipe(
map((data) =>
data.forEach((d) => {
let jobsData = d['_document']['data']['value']['mapValue'][
'fields'
] as Job;
const newData = {
id: d.id,
title: jobsData.title,
subtitle: jobsData.subtitle,
description: jobsData.description,
publish: jobsData.publish,
img: jobsData.img,
} as Job;
return newData;
})
)
);
}
This should work.
fetchJobs(): Observable<Job[]> {
const jc = collection(this.firestore, 'jobs')
return collectionSnapshots(jc)
.pipe(
map((snapshots) =>
snapshots.map((snapshot) => {
return { ...snapshot.data(), id: snapshot.id } as Job
})
)
)
}
which is equivalent to:
fetchJobs(): Observable<Job[]> {
const jc = collection(this.firestore, 'jobs')
return collectionData(jc, { idField: 'id' })
.pipe(
map((data) => data as Job[])
)
}
Since you only need to fetch the Job's data, collectionData() is way more appropriate.
collectionSnapshots() may be interesting when you need to perform additional operations on each Job, such as updating/deleting each one of them, which is possible with snapshot.ref
Example:
fetchJobs() {
const jc = collection(this.firestore, 'jobs')
return collectionSnapshots(jc)
}
deleteAllJobs() {
fetchJobs()
.pipe(take(1))
.subscribe(snapshots =>
snapshots.map((snapshot) => {
deleteDoc(snapshot.ref)
})
)
}
This is a mere example and the logic may not apply to your use case.

React front end not updating after delete operation in MERN application

I have a MERN application where I'm trying to delete a note (consisting of a title and content) when the delete button is clicked. When I click the button, the backend mongoDB database is updated - the item is, in fact, deleted - but then the console spits out an error.
Here's the delete operation, traced through the relevant files:
// App.jsx
const deleteNote = id => {
NoteDataService.deleteNote(id)
.then(response => {
console.log(`note ${id} deleted`);
setNotes(prevState => {
return prevState.notes.filter(note => note._id !== id);
});
})
.catch(e => console.log(e));
};
// note.js
import http from '../http-common.js';
class NoteDataService {
createNote = data => http.post('/', data);
getAll = () => http.get('/');
deleteNote = id => http.delete(`?id=${id}`);
}
export default new NoteDataService();
// http-common.js
import http from '../http-common.js';
class NoteDataService {
createNote = data => http.post('/', data);
getAll = () => http.get('/');
deleteNote = id => http.delete(`?id=${id}`);
}
export default new NoteDataService();
And here's the error:
App.jsx:41 Uncaught TypeError: Cannot read property 'filter' of undefined
at App.jsx:41
Of the CRUD operations I'm attempting to implement, it seems only the "Read" (get) functionality is working properly to initially populate my list of notes from the backend (using useEffect() on App component load).
Before the component mount your state notes will be null. Do check notes in your state is not null and it's an array will be fine
prevState.notes && prevState.notes.filter(note => note._id !== id)
This works...
const deleteNote = id => {
NoteDataService.deleteNote(id)
.then(response => {
console.log(`note ${id} deleted`);
console.log(notes);
setNotes(notes.filter(note => note._id !== id));
})
.catch(e => console.log(e));
};

Best way to access data in react

PROBLEM:
I have a MERN application that is has a model with a couple of other models in it. The problem that I figured out later is that it saves the _id of the object and not the actual object in the model when you do this
const checkoutHistory = new Schema({
book: { type: mongoose.Schema.Types.ObjectId, ref: 'books',required: true },
checkoutCopiesNum: {type: Number, required: true},
profChosen: { type: mongoose.Schema.Types.ObjectId, ref: 'prof', required: true },
dueDate: {type: String, required: true}
})
The book: part of the object when retreived will be an id some string like "DKKLDFJhdkghhe839kdd" whatever. This is fine because then I guess I can make an API call in the react app later to search for this book. Is this the correct way to do it though?
The other way that I thought of was in the actual endpoint that retrieves the data was to call the findByID functions and set that data. It didn't work though here is the code for that:
const checkoutHistoryMiddle = async (req, res, next) => {
try {
//get the body of the request
const body = req.body
//check for data
if(!body){
return res.status(400).json({
success: false,
error: 'no body given'
})
}
const history = new CheckoutHist(body)
console.log(history)
// await Book.findById({_id: history.book}, (err, book) => {
// history.book = book
// })
// await Prof.findById({_id: history.profChosen}, (err, prof) => history.profChosen = prof)
console.log(history)
history.save().then(() => next()).catch(error => {
return res.status(400).json({
success: false,
message: error,
msg: "checkout save failed"
})
})
} catch (error) {
res.status(400).json({
success: false,
message: error,
msg: "checkoutHist failed"
})
}
}
I commented out the part I was talking about because well, it didn't work. It still saved the id instead of the object. Which like I said is fine. I gave my other idea a go and decided to do the calls inside the react app.
So I first got the array of objects from the schema provided above like this:
const [bookHist, setBookHist] = useState()
useEffect( () => {
const getHistory = async () => {
api.getCheckoutHist().then(hist => {
setBookHist(hist.data.data.filter((data) => data.book === props.book_id))
})
}
getHistory()
}, [])
This will create an array of objects in bookHist that looks like this
[{_id: "DKJFDKJDKLFJSL", book: "LDKhgajgahgelkji8440skg", checkoutCopiesNum: 3, profChosen: "gjellkdh39gh39kal930alkdfj", dueDate: "11/11/11"}, {...}]
so the next step would be to take each item in the array and get the id to search the database with so api.findProfByID(bookHist[0].profChosen)
then I would need to update the state of bookHist somehow only that item without effect the other items in the array.
The questions I have are what is the best way to update one item in the array state?
How do I make so many api calls? how do I make sure that they are waited on so that the state actually changes once the calls complete?
Here are things I have tried so far:
useEffect(() => {
bookHist.map(async bHist => {
await Axios.get("http://localhost:8174/user/professor/" + bHist.profChosen).then(async prof => {
// console.log(prof)
// console.log(prof)
bHist.profChosen = prof.data.data
// setBookHist(prevStat => ({}))
// setBookHist(...bookHist, [bookHist.])
})
setBookHist(bHist)
})
}, [])
this didn't work I assume because it would not update the state because it is not waiting on the map to finish before it sets the state of bookHist
So then I searched on the internet and found a promisAll method in react like this:
useEffect(() => {
const change = async () => {
if(bookHist){
console.log("prof")
//get the prof data
// const galleries = []
await Promise.all(bookHist.map( (bHist, index) => {
return await Axios.get("http://localhost:8174/user/professor/" + bHist.profChosen);
})).then(someData => {
console.log(someData)
});
}
change()
}, [])
This also does not work for unknown reasons. It only works if it hot reloads and does not refresh. The logging actually logs something when it hot refreshes.
here is the entirety of the funcitional component:
import React, {useState, useEffect} from 'react'
import api from '../../api/index'
import Axios from 'axios'
export default function CheckoutBookHistroy(props){
const [bookHist, setBookHist] = useState()
const [histData, setHistData] = useState([{
book: {},
prof: {}
}])
useEffect( () => {
const getHistory = async () => {
api.getCheckoutHist().then(hist => {
setBookHist(hist.data.data.filter((data) => data.book === props.book_id))
})
}
getHistory()
}, [])
//i also tried this way but this resulted in an infinite loop
const [profChosen, setProfChosen] = useState()
const handleProfFind = async (id) => {
await Axios.get("http://localhost:8174/user/professor/" + id).then(prof => {
setProfChosen(prof.data.data)
})
}
return (
<div>
{
bookHist ?
bookHist.map(data => {
//need to present the prof data here for each data obj
return (
<div>Checked out {data.checkoutCopiesNum}</div>
)}) : <div>no data</div>
}
</div>
)
}
I really hope I can gain some insight into the correct way to do all of this. I must be either really close or awfully wrong. Thank you in advance!
just by looking at your code, i don't see too much issue, although your code is a bit convoluted.
some functions has no caller, ex. handleProfFind. One suggestion, if you want to do something, just do it, no need that many functions, ex.
// assume you only want to do it once after mounting
useEffect( () => {
if (!data) {
api.getCheckoutHist().then(hist => {
// you can set your data state here
// or you can get the id inside each item, and then call more APIs
// whatever you want to do, please finish it here
}
}
}, [])

Testing and mocking fetch in async useEffect and async Redux-Saga

I'm testing a functional component, that use React-Hooks and Redux-Saga. I can pass parameters in URL for the component, because they are a login page component.
My URL i pass is 'localhost/access/parameter', and when this parameter exists, i need to call a async redux saga, and if the fetch is OK, i put the result in redux-store. When the result is on redux-store, i have a useEffect that verify the result and if is OK, i put her in a input.
I can mock the result with axios, but i'm migrating to use only fetch. i mock the fetch, but when i use
mount(component), provided by enzyme, i do not how to await the redux-saga termine the request and useEffect do your job. I put a console log inside a effect, saga and log the input props to see your value prop, but the value is always empty . I tried to use setImmediate() and process.nextTick().
Links i use to write the code: 1,2, 3
I'm using formik, so they pass some props to me.
My component
const Login = ({
setFieldError, errors, response, fetchDomain, location, values, handleChange, handleBlur, setFieldValue, history,
}) => {
useEffect(() => {
async function fetchUrlDomain() {
const { pathname } = location;
const [, , domain] = pathname.split('/');
if (typeof domain !== 'undefined') {
await fetchDomain(domain);
}
}
fetchUrlDomain();
}, [fetchDomain, location]);
useEffect(() => {
if (typeof response === 'string') {
setFieldError('domain', 'Domain not found');
inputDomain.current.focus();
} else if (Object.keys(response).length > 0) {
setFieldValue('domain', response.Domain);
setFieldError('domain', '');
}
}, [response, setFieldValue, setFieldError]);
return (
<input name="domain" id="domain" value={values.domain} onChange={handleChange} onBlur={handleBlur} type="text" />
);
}
const LoginFormik = withFormik({
mapPropsToValues: () => ({ domain: '' }),
enableReinitialize: false,
validateOnBlur: false,
validateOnChange: false,
})(Login);
const mapStateToProps = () => ({ });
const mapDispatchToProps = dispatch => ({
fetchDomain: (value) => {
dispatch(action({}, constants.RESET_RESPONSE_DOMAIN));
dispatch(action(value, constants.FETCH_DOMAIN_REQUEST));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(LoginFormik);
My Saga
export function* fetchDomain(action) {
const url = yield `${mainUrl}/${action.payload}`;
try {
const response = yield fetch(url).then(res => res.json());
yield put(reduxAction(response , constants.FETCH_DOMAIN_SUCCESS));
} catch (e) {
yield put(reduxAction(e, constants.FETCH_DOMAIN_FAILURE));
}
}
My Reducer
case constants.FETCH_DOMAIN_FAILURE:
return { ...initialState, response: 'Domain not found' };
case constants.FETCH_DOMAIN_SUCCESS: {
const { payload } = action;
return {
...initialState,
id: payload.Id,
apis: payload.Apis,
response: payload,
};
}
case constants.RESET_RESPONSE_DOMAIN:
return { ...initialState };
My Test
it('input with fetch only', (done) => {
const mockSuccessResponse = {
Id: 'fafafafa',
Apis: [],
Domain: 'NAME',
};
const mockJsonPromise = Promise.resolve(mockSuccessResponse);
const mockFetchPromise = Promise.resolve({
json: () => mockJsonPromise,
});
global.fetch = jest.fn().mockImplementation(() => mockFetchPromise);
const wrapper = mount(
<Provider store={store}>
<LoginForm
history={{ push: jest.fn() }}
location={{ pathname: 'localhost/login/Domain' }}
/>
</Provider>,
);
process.nextTick(() => {
const input = wrapper.find('#domain');
console.log(input.props());
expect(input.props().value.toLowerCase()).toBe('name');
global.fetch.mockClear();
done();
});
});
I expect my input have value, but he don't. I tried to use jest-fetch-mock but just don't work, and i want to use native jest methods, no thirty party libraries.
I cannot say what's wrong with your current code. But want to propose different approach instead.
Currently you are testing both redux part and component's one. It contradicts with unit testing strategy when ideally you should mock everything except module under the test.
So I mean if you focus on testing component itself it'd be way easier(less mocks to create) and more readable. For that you need additionally export unwrapped component(Login in your case). Then you can test only its props-vs-render result:
it('calls fetchDomain() with domain part of location', () => {
const fetchDomain = jest.fn();
const location = { pathName: 'example.com/path/sub' }
shallow(<Login fetchDomain={fetchDomain} location={location} />);
expect(fetchDomain).toHaveBeenCalledTimes(1);
expect(fetchDomain).toHaveBeenCalledWith('example.com');
});
it('re-calls fetchDomain() on each change of location prop', () => {
const fetchDomain = jest.fn();
const location = { pathName: 'example.com/path/sub' }
const wrapper = shallow(<Login fetchDomain={fetchDomain} location={location} />);
fetchDomain.mockClear();
wrapper.setProps({ location: { pathName: 'another.org/path' } });
expect(fetchDomain).toHaveBeenCalledTimes(1);
expect(fetchDomain).toHaveBeenCalledWith('another.org');
});
And the same for other cases. See with this approach if you replace redux with direct call to fetch() or whatever, or if you refactor that data to come from parent instead of reading from redux store you will not need to rewrite tests removing mocks to redux. Sure, you will still need to test redux part but it also can be done in isolation.
PS and there is no profit to await fetchDomain(...) in useEffect since you don't use what it returns. await does not work like a pause and that code may rather confuse reader.

How to fetch (Express) data ONLY once the (React) form-submitted data has been successfully received and served?

I'm currently building a league of legends (a MOBA or multiplayer online battle arena game) search-based web app that essentially allows the user to search for their summoner's name and obtain general information regarding their search input. (The data is provided by the game's own third-party api)
I've been able to successfully retrieve the form data and perform the intended backend processes, however, upon the client's initial render, my results-listing component is already trying to fetch the nonexistent processed data.
How do I prevent the server request from firing until the server has actually successfully served the data?
(abridged single-component client example)
the summoner data endpoint is set to http://localhost:3001/api/summoner
server does not contain any additional routes
const App = () => {
const [summName, setSummName] = useState('');
const summonerFormData = new FormData();
// let data;
const findSummoner = () => {
summonerFormData.set('summonerName', summName);
}
// problem here
const data = axios.get('http://localhost:3001/api/summoner');
// axios.get('http://localhost:3001/api/summoner')
// .then(res => {
// data = res;
// });
return (
<div>
<form
method="POST"
action="http://localhost:3001/api/summoner"
onSubmit={findSummoner}
>
<input
value={summName}
name="summName"
onChange={e => setSummName(e.target.value)}
/>
<button type="submit">submit</button>
</form>
{data !== undefined &&
<div className="results">
data.map(match => {
<div>
<p>{match.kills}</p>
<p>{match.deaths}</p>
<p>{match.assists}</p>
</div>
})
</div>
}
</div>
)
}
Here's the Repo for some more context, but please don't hesitate to ask if you need more information or have any questions at all!
I really appreciate any help I can get!
Thanks!
Edits:
I've also tried using the useEffect hook considering the lifecycle point I'm trying to fetch would be componentDidMount, but wasn't quite sure what the solution was. Doing more research atm!
Close, but no cigar. Request gets stuck at 'pending'.
let data;
const fetchData = () => {
axios.get('http://localhost:3001/api/summoner');
};
useEffect(() => {
if (summName !== '') {
fetchData();
}
}, summName);
I tried putting the axios request within an async function and awaiting on the request to respond, and it seems to be working, however, the server is still receiving undefined when the client starts, which then is attempting to be fetched, never allowing the promise to be fulfilled.
const fetchData = async () => {
await axios
.get('http://localhost:3001/api/summoner')
.then(res => {
data = res;
})
.catch(() => {
console.log('error');
});
};
useEffect(() => {
fetchData();
}, [])
So I took the advice and recommendations from #imjared and #HS and I'm literally so close..
I just have one more problem... My data-mapping component is trying to map non-existent data before actually receiving it, giving me an error that it's unable to map match of undefined..
const [modalStatus, setModalStatus] = useState(false);
const [loading, setLoading] = useState(false);
const [data, setData] = useState({ hits: [] });
const [summName, setSummName] = useState('');
const [summQuery, setSummQuery] = useState('');
const summonerFormData = new FormData();
const prepareResults = async () => {
await setSummQuery(summName);
};
const findSummoner = async () => {
setLoading(true);
setModalStatus(false);
await summonerFormData.set('summonerName', summQuery);
};
useEffect(() => {
const fetchData = async () => {
if (summQuery) {
setData({ hits: [] });
console.log('fetching');
await axios
.get('http://localhost:3001/api/summoner')
.then(res => {
setData(res.data);
setLoading(false);
setModalStatus(true);
return data;
})
.catch(error => {
console.log(error);
});
}
};
fetchData();
}, [summQuery]);
SUCCESS! Thank you guys! Here's what ended up working for me:
const findSummoner = async () => {
setSummQuery(summName);
};
useEffect(() => {
setData({ hits: [] });
summonerFormData.set('summonerName', summQuery);
const fetchData = async () => {
setModalStatus(false);
setLoading(true);
if (summQuery !== '') {
setLoading(true);
console.log('fetching');
await axios
.get('/api/summoner')
.then(res => {
setData({
hits: res.data,
});
setError(false);
setLoading(false);
setModalStatus(true);
return data;
})
.catch(() => {
setError(true);
console.log('error');
});
}
};
if (summQuery !== '') {
fetchData();
}
}, [summQuery]);
This flow will help you design better -
1. User - input
2. Hit - search
3. Set loading in state - true,
5. Set data in state - empty
6. Call api
7. Get data
8. Then, set data in state
6. Set loading in state - false
Along the side in the render/return -
1. if loading in the state - indicate loading.
2. if loading done( false ) and data is not empty - show data.
3. if loading done and data is empty - indicate 'not-found'.
Coming to the initial render part - the axios.get() calls the api, which should only be initiated once the form is submitted in the case. Therefore, that logic should be moved inside the event-handler.

Resources