Everyone.
I'm trying to make a script that creates data in Cloud Firestore and returns it. But I can't access the data (undefined), why?
That's my script:
const tools = require('./bin/firebase');
const app = require('express')();
// eslint-disable-next-line new-cap
const server = require('http').Server(app);
const io = require('socket.io')(server);
server.listen(3000);
const db = tools.connectFirebase();
io.on('connection', (socket) => {
const ipAddress = socket.handshake;
console.log(ipAddress.address);
db.collection('socket').doc('refresh').collection('2').doc('1').set({
name: 'Los Angeles',
});
db.collection('socket').doc('refresh').onSnapshot((doc) => {
console.log('Current data: ', doc.data());
});
socket.on('disconnect', () => {
console.log('Disconnected.');
});
});
And that's what this script returns:
Image
And finally, that's the Cloud Firestore data that I wanted to return: Image
I need to return those nodes, so: Basically, I need something like this:
{
1: {
name: "Los angeles:",
},
2: {
name: "Washington DC",
},
}
Basically, I need to fix it, anyone can help me to deal with it?
(Sorry about my English skills, I'm trying to improve it)
Try this below. Your set method is returning before the data is written. It's returning a future(promise). You have to wait on it either with .then() or await to be able to read the data back.
db.collection('socket').doc('refresh').collection('2').doc('1').set({
name: 'Los Angeles',
}).then(() => db.collection('socket').doc('refresh').onSnapshot((doc) => {
console.log('Current data: ', doc.data());
}));
Edit: oh sorry, you are reading /socket/refresh, which is probably empty. The subcollections under it are not sent as a json as you described above. You have to do db.collection('socket').doc('refresh').collection('2').doc('1') to read them. You cannot get a subtree as a json like you expected.
Go ahead and put some data into /socket/refresh and hopefully it's not going to be undefined anymore.
Related
I'm learning about RTK Query and really confused. I'd be happy if someone could point me towards the right direction. My question is how one can manipulate the state of the application store the same way as it is done when using createAsyncThunk and setting up extraReducers.
export const asyncApiCall = createAsyncThunk("api/get_data", async (data) => {
const config = {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
}
};
const res = await axios.get( "http://apiserver/path_to_api",data,config );
return res['data']
} )
export const mySlice = createSlice({
name:"mySliceName",
initialState:{
data: [],
loadInProgress: false,
loadError: null,
extraData: {
// something derived based on data received from the api
}
},
extraReducers: {
[asyncApiCall .pending]: (state) => {
state.loadInProgress = true;
},
[asyncApiCall .fulfilled]: (state,action) => {
state.loadInProgress = false;
state.data = action.payload;
state.extraData = someUtilFunc(state.data)
},
[asyncApiCall.rejected]: (state) => {
state.loadInProgress = false;
state.loadError= true;
},
}
})
Now I'm replacing it with RTK Query. My current understanding is that RTK Query automatically generates hooks for exposing data received from the api and all the query-related info like if it's pending, if an error occurred etc.
import { createApi, fetchBaseQuery } from '#reduxjs/toolkit/query/react'
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: builder => ({
getData: builder.query({
query: () => '/get_data'
}),
setData: builder.mutation({
query: info => ({
url: '/set_data',
method: 'POST',
body: info
})
})
})
})
export const { useSendDataMutation, useGetDataQuery } = apiSlice
If I want to store some additional data that may be affected by the api calls should I create another slice that will somehow interact with the apiSlice, or is it possible to incorporate everything in this existing code? I'm sorry for possible naivety of this question.
The short answer is that RTK Query is focused on purely caching data fetched from the server. So, by default, it stores exactly what came back in an API call response, and that's it.
There are caveats to this: you can use transformResponse to modify the data that came back and rearrange it before the data gets stored in the cache slice, and you can use updateQueryData to manually modify the cached data from other parts of the app.
The other thing to note is that RTK Query is built on top of standard Redux patterns: thunks and dispatched actions. Every time an API call returns, a fulfilled action gets dispatched containing the data. That means you can also apply another suggested Redux pattern: listening for that action in other reducers and updating more than one slice of state in response to the same action.
So, you've got three main options here:
If the "extra data" is derived solely from the server response values, you could use transformResponse and return something like {originalData, derivedData}
You could just keep the original data in the cache as usual, but use memoized selector functions to derive the extra values as needed
If you might need to update the extra values, then it's probably worth looking at listening to a query fulfilled action in another slice and doing something with it, like this silly example:
import { api } from "./api";
const someExtraDataSlice = createSlice({
name: "extraData",
initialState,
reducers: {/* some reducers here maybe? */},
extraReducers: (builder) => {
builder.addMatcher(api.endpoints.getPokemon.matchFulfilled, (state, action) => {
// pretend this field and this payload data exist for sake of example
state.lastPokemonReceived = action.payload.name;
}
}
})
I implemented a chatscreen inside my app and the following code represents the important sample of the code and I noticed that something about the data usage is very odd. The code is a little bit longer code sample but I will explain it after that.
const CountryChat = props =>{
var chosenLanguage = useSelector(state => state.myLanguage.myLanguage);
const countryId = props.navigation.getParam("countryId");//already upper case so no worries about correct firestore adress
const countryName = props.navigation.getParam("countryName");
const userId = useSelector(state => state.auth.userId);
const [TItext, setTItext] = useState("");
const [chatmessages, setChatMessages] = useState(() => []);//dummydata so FlatList wont crash because messages are empty during first renderprocess
const [refreshFlatlist, setRefreshFlatList] = useState(false);
const [myProfilePic, setMyProfilePic] = useState(null);
useEffect(() => {
downloadProfilePic();
var loadnewmessages = firebase.firestore().collection("group_rooms").doc("group_rooms").collection(`${countryId}`).orderBy("timestamp").limit(30).onSnapshot((snapshot) => {
var newmessages = [];
var deletedmesssages = [];
snapshot.docChanges().forEach((change) => {
if(change.type === "added"){
newmessages.push({
counter: change.doc.data().counter,
sender: change.doc.data().sender,
timestamp: change.doc.data().timestamp.toString(),
value: change.doc.data().value,
displayedTime: new Date(change.doc.data().displayedTime),
senderProfilePic: change.doc.data().senderProfilePic
})
};
if(change.type === "removed"){
deletedmesssages.push({
counter: change.doc.data().counter,
sender: change.doc.data().sender,
timestamp: change.doc.data().timestamp.toString(),
value: change.doc.data().value,
displayedTime: new Date(change.doc.data().displayedTime),
senderProfilePic: change.doc.data().senderProfilePic
})
};
})
if(newmessages.length > 0){
setChatMessages(chatmessages => {
return chatmessages.concat(newmessages)
});
};
if(deletedmesssages.length > 0){
setChatMessages(chatmessages => {
var modifythisarray = chatmessages;
let index = chatmessages.map(e => e.timestamp).indexOf(`${deletedmesssages[0].timestamp}`);
let pasttime = Date.now() - parseInt(modifythisarray[index].timestamp);
modifythisarray.splice(index, 1);
if(pasttime > 300000){
return chatmessages
}else{
return modifythisarray
}
});
setRefreshFlatList(refreshFlatlist => {
//console.log("Aktueller Status von refresher: ", refreshFlatlist);
return !refreshFlatlist
});
}
newmessages = [];
deletedmesssages = [];
});
return () => { //for removing listeners
try{
loadnewmessages();
}catch(error){console.log(error)};
}
}, []);
const pushMessagetoDB = async (filter, imageName) => {
//sending message to the chatroom in Firestore
if(filter == 1){
await firebase.firestore().collection("group_rooms").doc("group_rooms").collection(`${countryId}`).add({
"counter": 1,
"sender": userId,
"timestamp": Date.now(),
"value": TItext,
"displayedTime": (new Date()).toISOString(),
"senderProfilePic": myProfilePic
})
.then(() => {
console.log("Chat written in DB!");
})
.catch((error) => {
console.error("Error writing Chat into DB: ", error);
});
}else{
await firebase.firestore().collection("group_rooms").doc("group_rooms").collection(`${countryId}`).add({
"counter": 2,
"sender": userId,
"timestamp": Date.now(),
"senderProfilePic": myProfilePic,
"value": await firebase.storage().ref(`countrychatimages/${countryId}/${imageName}`).getDownloadURL().then((url) => {
return url
}).catch((error) => { //incase something bad happened
console.log(error);
})
})
.then(() => {
console.log("Image passed to DB!");
})
.catch((error) => {
console.error("Error passing Image to DB: ", error);
});
}
};
What you can see here is my listener loadnewmessages which is beeing called inside my useEffect. This listener downloads the recent 30 messages in the chat and stores them in a state. The chat works perfect and I can even send a message (store a document on the firestore inside a collection which represents the chat). After I leave the screen the return in the useEffect is fired and my listener is getting canceled.
My problem is: I went back and forth around 4 times and I had 6 messages in my collection. After I did that I closed my app and checked my usage in "Usage and billing" in firebase and saw that suddenly I had around 25 reads. I was expecting that my listener will only download the collection with the documents once and will maintain in on the phone even if I leave the screen, not that I redownload it always when I check the screen, that is what I assume is happening after I saw my usage in my firebase console. If I launch my app and I receive 100 or more users, my billings will explode this way.
I know that I detach my listener and relaunch it but I expected firebase to maintain the already loaded data on the phone so I (if no new files will be written) I only get 1 read because the query run without loading any new data.
Can somebody pls explain to me what I did wrong or how I could improve my code to shrink down the reads? How can I change my code so it stays efficient and does not download already loaded data? Its really important for me to maintain the reads on a low level, I have big problems getting this under control and my money is very limited.
That is the intended behavior. When you switch your pages/activities the listener is closed. A listener will fetch all the matching documents specified in query when it's reconnected (just like being connected for first time) as mentioned in the docs:
An initial call using the callback you provide creates a document snapshot immediately with the current contents of the single document. Then, each time the contents change, another call updates the document snapshot.
You can try:
Enabling offline persistence which caches a copy of the Cloud Firestore data that your app is actively using, so your app can access the data when the device is offline. If the documents are fetched from the cache then you won't be charged reads. However I am not sure if this will be the best option for your use case.
Storing messages fetched so far in local storage of that platform and then query messages sent after message using the listener. You would have to remove messages from local storage if any message is deleted.
const messagesRef = db..collection("group_rooms").doc("group_rooms").collection(`${countryId}`);
return messagesRef.doc("last_local_msg_id").get().then((doc) => {
// Get all messages sent after last local msg
const newMessagesQuery = messagesRef
.orderBy("timestamp")
.startAt(doc)
.limit(30);
});
Using for example async storage suits good, even increasing the size of the memory of async storage is not a problem so that its possible to store more data and therefore more chats as showed here.
I am trying to display information about users in a group in a list. I want to gather additional information about each member which is stored under users/ in my database.
My code manages to gather information but only for the first member. To get the others members data to load, I need to navigate back to a different screen then go back.
Here is what I tried so far:
//members object from group/id/members is passed to this function
//e.g. [{"uid":"abc","type":"admin"},{"uid":"def","type":"member"},{"uid":"ghi","type":"member"}]
getMembersData(members) {
const membersData = {};
Object.keys(members).map((key, i) => {
const member = members[key];
firebase.database().ref(`users/${member.uid}`).on('value', snap => {
membersData[member.uid] = {
'uid': member.uid,
'username': snap.child('username').val(),
'imageUrl': snap.child('imageUrl').val(),
'type': member.type,
};
});
});
this.setState({ membersData });
}
In other words, when I navigate to the members screen, only the information for the first member is rendered when I map this.state.membersData in a View, until I back out from the screen and go back to it.
The code works fine if I cut out the firebase content. How can I fix this issue so that everything loads when I navigate to the screen?
I think you'll want to call setState inside the database callback since the callback is asynchronous and won't be invoked until the data is finally available.
firebase.database().ref(`users/${member.uid}`).on('value', snap => {
membersData[member.uid] = {
'uid': member.uid,
'username': snap.child('username').val(),
'imageUrl': snap.child('imageUrl').val(),
'type': member.type,
};
this.setState(membersData);
});
});
There is no guarantee how long it will take to get the data, so your code should be ready to deal with the fact that it might take a while to arrive (or possibly never at all if there is no internet connection).
You want to call this.setState once all of your database calls have finished. If you only need to download the data once, you can make use of chaining Promises.
Because you are using .map(), you can achieve this by returning the Promise from database requests that use .once('value'). In the code below, the variable named transformPromises is the array of Promises produced by .map() and .once('value'). Once you have this array of promises, you can use Promise.all(promiseArray).then(() => {...} to wait for them all to resolve. In the code below, this is where we then call this.setState() to update the user interface.
Before calling getMembersData() it may be useful to show a throbber while loading the data.
getMembersData(members) {
const membersData = {};
let transformPromises = Object.keys(members).map((key, i) => {
const member = members[key];
return firebase.database().ref(`users/${member.uid}`).once('value')
.then(snap => {
membersData[member.uid] = {
'uid': member.uid,
'username': snap.child('username').val(),
'imageUrl': snap.child('imageUrl').val(),
'type': member.type,
};
});
});
Promise.all(transformPromises)
.then(() => {
// all data was downloaded & added successfully.
this.setState({ membersData });
})
.catch((err) => {
// handle error
console.error(err)
});
}
Note: Based on the shape of members, you could also iterate them using the following code:
let transformPromises = members.map(member => {
return firebase.database().ref(`users/${member.uid}`).once('value')
.then(snap => {
membersData[member.uid] = {
'uid': member.uid,
'username': snap.child('username').val(),
'imageUrl': snap.child('imageUrl').val(),
'type': member.type,
};
});
}
I am trying to make an "onUpdate" function that loads the document that has been updated. Then I want to load another document using the data received by the wildcards. So to summarize I want to access the document that was updated and one more that is in the same collection.
I want : /userProfiles/{doc1}/employees/{doc2} AND /userProfiles/{doc1}.
I can get them both but when I try to use the data from one, it doesn't read the previous data and gives me a ReferenceError.
The end goal is to use both these docs to send an email with nodemailer. Thanks for any help.
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const nodemailer = require('nodemailer');
admin.initializeApp();
exports.testLog = functions.firestore
.document('/userProfiles/{doc1}/employees/{doc2}')
.onUpdate((change, context) => {
var info = [];
const doc1 = context.params.doc1;
const doc2 = context.params.doc2;
const db = admin.firestore();
return (
db
.collection("userProfiles")
.doc(`${doc1}`)
.get()
.then(doc => {
var email = doc.data().email;
var phone = doc.data().phone;
info.push(doc.data());
console.log(email, phone); // sees and gets info
return email, phone;
}),
db
.collection("userProfiles")
.doc(`${doc1}`)
.collection(`employees`)
.doc(`${doc2}`)
.get()
.then(doc => {
info.push(doc.data());
var Status = doc.data().Status;
console.log(phone, `${Status}`); //phone is undefined
if (`${Status}` === "Alarm") {
// replace with variables from the users settings page
console.log(`${info.phone}`); // phone is undefined
let transporter = nodemailer.createTransport({
host: "smtp.gmail.com",
port: 587,
secure: false,
auth: {
user: "xxxxxx#gmail.com",
pass: "xxxxxxxxxx"
}
});
// send mail with defined transport object
let mailOptions = {
from: '"Fred Foo 👻" <foo#example.com>',
to: `${info.phone}`, // tried phone as well
subject: "Hello ✔",
text: "216+?",
};
transporter.sendMail(mailOptions, error => {
if (error) {
return console.log(error);
} else {
return console.log("message sent");
}
});
}
console.log(Status);
// return
return console.log("im after the if statement. No alarm triggered");
})
.then(message => console.log(message.sid, "success"))
.catch(err => console.log(err))
);
});
So I want to get the phone number and the Status in these 2 images
The error that is returned:
ReferenceError: phone is not defined
There are two things that aren't quite working the way you expect leading to your problem:
The handling of promises isn't really passing data the way you expect -- in particular, the variables phone and email exist only in one promise handler, they aren't global in scope, so phone and email aren't being passed down the promise chain.
You don't actually need to ever read the second document, as the content is passed to you in the function itself. This actually greatly simplifies the overall thing you are doing, and makes dealing with the first point nearly trivial, since you can skip the second database call.
Look at this code where I have omitted the messaging code for clarity and just left in place most of the log messages:
exports.firestoreOnUpdateTest = functions.firestore
.document('/userProfiles/{doc1}/employees/{doc2}')
.onUpdate((change, context) => {
// var info = []; I have removed this list, it is not necessary
const doc1 = context.params.doc1;
// no need to get the doc2 parameter, as we are handed the doc itself by the function call.
const doc2content = change.after.data();
const db = admin.firestore();
return (
db
.collection("userProfiles")
.doc(`${doc1}`)
.get()
.then(doc => {
const doc1content = doc.data();
const email = doc1content.email;
const phone = doc1content.phone;
console.log(email, phone); // sees and gets info
console.log(`No need to fetch doc2, as I already have it: ${JSON.stringify(doc2content)}`);
const Status = doc2content.Status;
console.log(`email for user is still: ${email}`); // email is now defined
console.log(phone, `${Status}`); // phone is now defined
if (`${Status}` === "Alarm") {
console.log(`${phone}`); // phone is now defined
return console.log('message would be sent here - code omitted')
}
console.log(Status);
return console.log("im after the if statement. No alarm triggered");
})
.catch(err => console.error(err))
);
});
In the new version, we just store the content from the document that triggered us, including the Status parameter. We then fetch the document with the content we need -- at the higher level in the tree. Once that document is returned, we just process it and combine with the data from doc2. All the fields are now defined (assuming, of course, the database objects are well-formed).
Your messaging code would be re-inserted right were the obvious log message is.
Finally, the info list I don't think is necessary now, so I've removed it. Instead, I recommend you build what you need as you construct the message itself from the data already on hand. That said, your original code wasn't accessing it correctly (that is, as a list) anyway and may have been confusing you further.
Finally, I haven't addressed the use of the Nodemailer module as the question focused primarily on the undefined fields, but I suspect your original code may not be entirely correct either -- as it doesn't either return a promise back from sendMail() or perform an await on that call (and make the entire function async), so you will need to look at that more closely.
So, I have a Firestore database group like so.
companies > acme-industries > items > []
OR
collection > document > collection > document
Would it be better to just store all items inside a base collection and then add a string value to each item that defines what company it goes too? Then just query the items collection for all items linked to that company?
I am trying to retrieve the items and run them through a forEach in my firebase function. I have tried two different approaches and watched multiple videos and still am not getting results.
First Attempt Code Block
This resulted in a 500 Server Error with no explanation returned.
const itemQuerySnapshot = db.collection('companies').doc(data.userData.company).collection('items').get();
const items: any = [];
itemQuerySnapshot.forEach((doc:any) => {
console.log('doc', doc.data());
items.push({
id: doc.id,
data: doc.data()
});
});
response.json(items);
Second Attempt Code Block
This resulted in the No Such Documents! being returned
const itemRef = db.collection('companies').doc(data.userData.company).collection('items');
itemRef.get().then((doc:any) => {
if(!doc.exists) {
response.send('No such documents!');
} else {
response.send('Document Data: '+ doc.data());
}
}).catch((err:any) => {
response.status(500).send(err);
});
I am expecting something like an array of all the items to be returned from this call. I'm completely new to Firebase Firestore, what am I missing here?
UPDATE
I replaced my code with a third attempt code block and I got success with the console.log(doc.data()). However, the items object still returns empty. Is this because it's returning before the for each is done? If so, how would you prevent that to ensure every item that should be returned is?
const items: any = [];
const userRef = db.collection("companies").doc(data.userData.company);
const itemsRef = userRef.collection("items");
itemsRef
.get()
.then((snapshot: any) => {
snapshot.forEach((doc: any) => {
console.log(doc.data());
items.push({
id: doc.id,
data: doc.data()
});
});
})
.catch((err: any) => {
response.status(500).send(err);
});
response.json(items);
How would you add one more document into the mix? Say you want to get a single item. How would you do that? The following always results in Item does not exist being returned from my function.
const companyRef = db.collection('companies').doc(data.userData.company);
const itemRef = companyRef.collection('items');
const item = itemRef.where('number', '==', itemSku).get();
I must be doing something incredibly wrong here because all the videos are telling me it's incredibly easy to fetch data from Firestore. But I have yet to see that.
get returns a Promise , the callback of then function will be called once the data ready from firestore .
the line response.json(items); will be called before the items array collected correctly.
you need to move this line inside the then callback
checkout this :
.then((snapshot: any) => {
snapshot.forEach((doc: any) => {
console.log(doc.data());
items.push({
id: doc.id,
data: doc.data()
});
});
response.json(items); //items ARRAY IS READY , YOU CAN SEND YOUR RESPONSE HERE
})