Firebase callable function to read real time database - javascript

Here I am trying to access the user's data from real time database by providing the UID. I have tried so many things but none worked. I have followed the documentation but no luck I am keep getting error -
Sending back results [promise]
Another example for writing the data which I have followed to create my logic but it didn't worked -
exports.userData = functions.https.onCall((data, context) => {
// verify Firebase Auth ID token
if (!context.auth) {
return { message: 'Authentication Required!', code: 401 };
}
const userId = data.text;
const ref = database.ref('/USERS/' + userId);
return ref.on('value', (snapshot) => {
console.log(snapshot); /* <--- I have tried with this and without this none worked*/
})
.then(snapshot => {
return {
data: snapshot
};
}).catch((error) => {
throw new functions.https.HttpsError('unknown', error.message, error);
});
});
The error I get on client side is -
service.ts:160 POST https://us-central1-gokuapp.cloudfunctions.net/userData 500
error.ts:66 Uncaught (in promise) Error: INTERNAL
at new YN (error.ts:66)
at XN (error.ts:175)
at rC.<anonymous> (service.ts:231)
at tslib.es6.js:100
at Object.next (tslib.es6.js:81)
at r (tslib.es6.js:71)
Edit: Before, I was correctly writing the code on my end but, I was either getting the error or null object based on the changes that I made during the discovery process. Anyone who had faced the same problem, just remember this... "cloud functions takes time to warm up to get fully functional", even though I am really thankful to #Frank van Puffelen and #oug Stevenson for their input. :) :) :)

Don't use on() in Cloud Functions, since that attaches a persistent listener to a query (and it doesn't return a promise). Use once() instead to query data a single time and get a promise the resolves with a snapshot. Also you should use snapshot.val() to get a plain JavaScript object with the contents of the snapshot.
return ref.once('value') // use once() here
.then(snapshot => {
return {
data: snapshot.val() // also use val() here to get a JS object
};
})
.catch((error) => {
throw new functions.https.HttpsError('unknown', error.message, error);
});

Callable Cloud Functions can return any JSON data. Your snapshot variable is a DataSnapshot object however, which contains a lot more than just JSON data.
You're probably looking to return the snapshot's value:
.then(snapshot => {
return {
data: snapshot.val()
};

Related

Sending firebase database data to client through cloud function

I am trying to send data from my firebase database back to my client in a cloud function. I want to return the entire json child object. Here is the code for my cloud function:
exports.addNumbers = functions.https.onCall((data, context) => {
admin.database().ref('store/ingredients').once('value', function(snapshot) {
return snapshot.val();
});
});
Here is my client code that is invoking this cloud function and accessing its data:
const addNumbers = firebase.functions().httpsCallable('addNumbers');
addNumbers().then(result => {
console.log(result.data + "in client");
});
But the data returned in the client is null and the cloud function is returning only object [Object] in the firebase logs. Please someone help me.
You're almost there. You just need to return something from the top-level code in your Cloud Function.
The easiest way is to use a then clause:
exports.addNumbers = functions.https.onCall((data, context) => {
return admin.database().ref('store/ingredients').once('value').then((snapshot) => {
return snapshot.val();
});
});
Now the value from the database "bubbles up" to the calling code, and is then returned out of your function as a promise.
On modern JavaScript versions, you can make this code easier to read by using async/await:
exports.addNumbers = functions.https.onCall(async (data, context) => {
let snapshot = await admin.database().ref('store/ingredients').once('value')
return snapshot.val();
});
This works exactly the same under the hood (it's really just syntactic sugar), but most developers find it much easier to read.

Issue using Axios with Scryfall API

Attempting to use axios to make a get request at the following endpoint, and I keep getting errors:
When I check it using Postman and in a browser (GET request), it returns data just fine, but otherwise I can’t get a response.
This is the call I’m using, I don’t know if it’s some sort of issue with my code or with axios itself:
axios.get(`https://api.scryfall.com/cards/named?exact=${args.name}`)
.then((res) => {
console.log(JSON.stringify(res));
})
.catch((err) => {
if (err.response) {
throw new Error(`Card with name (${name}) not found!`)
}
throw new Error(`Could not complete that query!`)
})
The argument args.name is passed as part of a GraphQL resolver, and it definitely has a value, so not sure what the deal is.
Any help would be greatly appreciated!
There are a couple of problems here.
Generally it's not a good idea to throw new errors after catching the original error. As written, your code throws an additional error because the axios Promise is thrown again instead of being dealt with inside catch - so node will complain that you didn't resolve or reject the promise.
The substantive issue is the same as the one answered here except for res - the actual error is TypeError: Converting circular structure to JSON which is caused by trying to JSON.stringify the res object:
JSON doesn't accept circular objects - objects which reference themselves. JSON.stringify() will throw an error if it comes across one of these.
The request (req) object is circular by nature - Node does that.
In this case, because you just need to log it to the console, you can use the console's native stringifying and avoid using JSON
So you can fix this by changing your code to:
axios.get(`https://api.scryfall.com/cards/named?exact=${args.name}`)
.then((res) => {
console.log(res)
})
.catch((err) => {
console.log(err)
if (err.response) {
console.error(`Card with name (${name}) not found!`)
} else {
console.error(`Could not complete that query!`)
}
})
If you were just using console.log for testing/as an example and actually need to stringify the data to use some other way, just make sure you're stringifying the data (which is presumably what you actually want) and not the whole res object:
axios.get(`https://api.scryfall.com/cards/named?exact=${args.name}`)
.then((res) => {
let scryfallData = JSON.stringify(res.data)
doSomethingWith(scryfallData)
})

Firebase Cloud Functions: Transactions function not returning promise?

here is what I am trying to do with firebase cloud function:
-Listen to any change in one of the documents under 'user' collection.
-Update carbon copies of the userinfo in the relevant documents in both 'comment' and 'post' collections.
Because I will need to query in relevant documents and update them at once, I am writing codes for transaction operations.
Here is the code that I wrote. It returns the error message, 'Function returned undefined, expected Promise or value'.
exports.useInfoUpdate = functions.firestore.document('user/{userid}').onUpdate((change,context) => {
const olduserinfo=change.before.data();
const newuserinfo=change.after.data();
db.runTransaction(t=>{
return t.get(db.collection('comment').where('userinfo','==',olduserinfo))
.then((querysnapshot)=>{
querysnapshot.forEach((doc)=>{
doc.ref.update({userinfo:newuserinfo})
})
})
})
.then(()=>{
db.runTransaction(t=>{
return t.get(db.collection('post').where('userinfo','==',olduserinfo))
.then((querysnapshot)=>{
querysnapshot.forEach((doc)=>{
doc.ref.update({userinfo:newuserinfo})
})
})
})
})
});
I am a bit confused because as far as I know, 'update' method returns a promise? I might be missing something big but I picked up programming only last November, so don't be too harsh. :)
Any advice on how to fix this issue? Thanks!
EDIT:
Building on Renaud's excellent answer, I created the below code in case someone may need it.
The complication with transaction is that the same data may be stored under different indices or in different formats. e.g. The same 'map' variable can be stored under an index in one collection, and as part of an array in another. In this case, each document returned by querying needs different update methods.
I resolved this issue using doc.ref.path, split, and switch methods. This enables application of different update methods based on the collection name. In a nutshell, something like this:
return db.runTransaction(t => {
return t.getAll(...refs)
.then(docs => {
docs.forEach(doc => {
switch (doc.ref.path.split('/')[0]) { //This returns the collection name and switch method assigns a relevant operation to be done.
case 'A':
t = t.update(doc.ref, **do whatever is needed for this collection**)
break;
case 'B':
t = t.update(doc.ref, **do whatever is needed for this collection**)
break;
default:
t = t.update(doc.ref, **do whatever is needed for this collection**)
}
})
})
})
Hope this helps!
Preamble: This is a very interesting use case!!
The problem identified by the error message comes from the fact that you don't return the Promise returned by the runTransaction() method. However there are several other problems in your code.
With the Node.js Server SDK you can indeed pass a query to the transaction's get() method (you cannot with the JavaScript SDK). However, in your case you want to update the documents returned by two queries. You cannot call twice db.runTransaction() because, then, it is not a unique transaction anymore.
So you need to use the getAll() method by passing an unpacked array of DocumentReferences. (Again, note that this getAll() method is only available in the Node.js Server SDK and not in the JavaScript SDK).
The following code will do the trick.
We run the two queries and transform the result in one array of DocumentReferences. Then we call the runTransaction() method and use the spread operator to unpack the array of DocumentReferences and pass it to the getAll() method.
Then we loop over the docs and we chain the calls to the transaction's update() method, since it returns the transaction.
However note that, with this approach, if the results of one of the two original queries change during the transaction, any new or removed documents will not be seen by the transaction.
exports.useInfoUpdate = functions.firestore.document('user/{userid}').onUpdate((change, context) => {
const olduserinfo = change.before.data();
const newuserinfo = change.after.data();
const db = admin.firestore();
const q1 = db.collection('comment').where('userinfo', '==', olduserinfo); // See the remark below: you probably need to use a document field here (e.g. olduserinfo.userinfo)
const q2 = db.collection('post').where('userinfo', '==', olduserinfo);
return Promise.all([q1.get(), q2.get()])
.then(results => {
refs = [];
results.forEach(querySnapshot => {
querySnapshot.forEach(documentSnapshot => {
refs.push(documentSnapshot.ref);
})
});
return db.runTransaction(t => {
return t.getAll(...refs)
.then(docs => {
docs.forEach(doc => {
t = t.update(doc.ref, { userinfo: newuserinfo })
})
})
})
})
});
Two last remarks:
I am not sure that db.collection('comment').where('userinfo', '==', olduserinfo); will be valid as olduserinfo is obtained through change.before.data(). You probably need to specify one field. This is probably the same for newuserinfo.
Note that you cannot do doc.ref.update() in a transaction, you need to call the transaction's update() method, not the one of a DocumentReference.

React + Firestore : Return a variable from a query

I'm learning React and Firestore currently and am a bit stuck. I'm trying to retrieve a users name from a firestore collection by searching their uid.
The following code is executed in a map of 'lessons' to create a list.
{lesson.post_author && findName(lesson.post_author)}
The following code is the findName function.
let findName = uid => {
firebase.firestore().collection("users")
.where('uid', '==', uid)
.get()
.then(querySnapshot => {
console.log(querySnapshot.docs[0].data().name);
});
};
Currently, the findName function will console log all of the names to the console successfully. I've altered the code to be able to console log outside of the firestore call, but that returns a promise pending in console.
The goal of the code is to return the name rather then the uid in the list.
Any help would be much appreciated.
Thank you!
As others have explained, you can't return that value, since it's loaded from Firestore asynchronously. By the time your return runs, the data hasn't loaded yet.
In React you handle this by putting the data in the component's state, and using it from there. If you do this, your render method can simply pick it up from the state, with something like:
{lesson.post_author && findName(lesson.post_author_name)}
(the above assumes that lesson indirectly comes from the state.
It's a bit easier if we pretend there's only one lesson, and you have these values straight in the state:
{state.post_author && findName(state.post_author_name)}
Now I'll assume you already have the post_author and you just need to look up the author's name. That means that somewhere in/after componentDidMount you'll load the additional data and add it to the state:
componentDidMount() {
firebase.firestore().collection("users")
.where('uid', '==', this.state.uid)
.get()
.then(querySnapshot => {
this.setState({ post_user_name: querySnapshot.docs[0].data().name });
});
}
Now the loading of the data still happens asynchronously, so the call to setState() happens some time after componentDidMount has completed. But React is aware that changing the state may require a refresh of the component, so it responds to the call to setState() by rerendering it.
Note that I'd highly recommend using each user's UID as the ID of the documents in users. That way you don't need a query and can just do a directly lookup:
componentDidMount() {
firebase.firestore().collection("users")
.doc(this.state.uid)
.get()
.then(doc => {
this.setState({ post_user_name: doc.data().name });
});
}
I'm trying to retrieve a users name from a firestore collection by
searching their uid.
This is accomplished by using the asyncronous .get method on a Firestore reference. In your case, you probably have a users collection of firebase.auth().currentUser.uid named documents.
var userRef = firebase.firestore().collection('users').doc(users.uid);
userRef.get().then(function(doc) {
if (doc.exists) {
console.log("Users first name is:", doc.data().firstName);
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
});

Observable - 401 causing forkJoin to error out

I am using forkJoin to make several server requests. This is a pattern I have commonly been using through out my application and it has been working great. However we just started implementing user roles which is done on the backend. I am not sure what is the best practice for implementing roles as I am mostly a front end developer, nonetheless this is the problem I have encountered:
Our application has member and admin member roles.
From each view I must make calls to the backend for both member and admin member roles regardless as roles are not determined on the frontend.
Member data is always returned in for both roles as members and admin members both have personal data.
Requests made for admin data is only returned when the user is an admin. Whenever the user does not have admin access the request returns a 401 error. This is where I am having a problem.
Whenever the call returns a 401, the error method in my subscribe method is invoked and I do not have access to any of the calls that were made including the calls associated to the member data.
In my included code within the forkJoin there are five calls passed into the method. The third and forth call only return data if the user is an admin while the rest of the calls are always returned for either member or admin.
When the user is not an admin the third call returns a 401 and the stream stops and the error handler in my subscribe method is invoked. This is obviously not what I want. I want the stream to continue so I can use the data in the _data method.
I have only been using RXJS for 6 months and am learning. Maybe I should be using a different pattern or maybe there is a way to fix this. Any help with code examples would be greatly appreciated. Below my code example I included another example of code in which I attempted to fix the problem by playing around with catch methods. It didn't work.
My View get method:
private getZone() {
this.spinner.show();
this.zonesService.getZone(this.zoneId)
.map(response => {
this.zone = response['group'];
return this.zone;
})
.flatMap(() => {
return Observable.forkJoin(
this.teamsService.getTeam(this.zone['TeamId']),
this.zonesService.getZoneAssociations(this.zone['id'], '/myDevices'),
this.zonesService.getZoneAssociations(this.zone['id'], '/devices'),
this.zonesService.getZoneAssociations(this.zone['id'], '/groupMembers'),
this.sitesService.getSite(this.zone['SiteId'])
);
})
.subscribe(
_data => {
// data handling...
},
_error => {
// error handling ...
}
);
}
My attempt to fix:
private getZone() {
this.spinner.show();
this.zonesService.getZone(this.zoneId)
.map(response => {
this.zone = response['group'];
return this.zone;
})
.flatMap(() => {
return Observable.forkJoin(
this.teamsService.getTeam(this.zone['TeamId']),
this.zonesService.getZoneAssociations(this.zone['id'], '/myDevices'),
this.zonesService.getZoneAssociations(this.zone['id'], '/devices')
.catch(error => Observable.throw(error)),
this.zonesService.getZoneAssociations(this.zone['id'], '/groupMembers')
.catch(error => Observable.throw(error)),
this.sitesService.getSite(this.zone['SiteId'])
);
})
.subscribe(
_data => {
// data handling...
},
_error => {
// error handling...
}
);
}
Returning Observable.throw will just rethrow the caught error, which will see forkJoin emit the error.
Instead, you could use Observable.of(null) to emit null and then complete, which will see forkJoin emit a null for the observable that emitted the error:
return Observable.forkJoin(
this.teamsService.getTeam(this.zone['TeamId']),
this.zonesService.getZoneAssociations(this.zone['id'], '/myDevices'),
this.zonesService.getZoneAssociations(this.zone['id'], '/devices')
.catch(error => Observable.of(null)),
this.zonesService.getZoneAssociations(this.zone['id'], '/groupMembers')
.catch(error => Observable.of(null)),
this.sitesService.getSite(this.zone['SiteId'])
);
Or, if you wanted to emit the error as a value, you could use Observable.of(error).

Categories