When trying to deal with an array of references I get the error "FirebaseError: Function Firestore.doc() requires its first argument to be of type non-empty string, but it was: a custom t object"
My user doc has an array of references called reviews and I am trying to get the data of each of the references.
const handleFetch = async () => {
let db = firebase.firestore();
let userRef = await db
.collection("users")
.doc(props.user.id.uid) //<- doc works and returns correctly
.get();
userRef.data().reviews.forEach((ref, indx) => {
let hold = db.doc(ref).get(); //<- error occurs here
});
}
In firestore, a Reference object is constructed into a document or collection reference and you can invoke any of the document methods on it as needed.
const ref = snapshot.data().reviews[0];
ref.get();
ref.remove();
The error in particular is saying that the item is not a string, which if it is a firestore reference, is an object and thus, not compatible.
A great video to watch: https://www.youtube.com/watch?v=Elg2zDVIcLo&t=276s
Related
I'm using Firebase as backend to my iOS app and can't figure out how to construct a batch write through their Cloud Functions.
I have two collections in my Firestore, drinks and customers. Each new drink and each new customer is assigned a userId property that corresponds to the uid of the currently logged in user. This userId is used with a query to the Firestore to fetch only the drinks and customers connected to the logged in user, like so: Firestore.firestore().collection("customers").whereField("userId", isEqualTo: Auth.auth().currentUser.uid)
Users are able to log in anonymously and also subscribe while anonymous. The problem is if they log out there's no way to log back in to the same anonymous uid. The uid is also stored as an appUserID with the RevenueCat SDK so I can still access it, but since I can't log the user back in to their anonymous account using the uid the only way to help a user access their data in case of a restoring of purchases is to update the userId field of their data from the old uid to the new uid. This is where the need for a batch write comes in.
I'm relatively new to programming in general but I'm super fresh when it comes to Cloud Functions, JavaScript and Node.js. I dove around the web though and thought I found a solution where I make a callable Cloud Function and send both old and new userID with the data object, query the collections for documents with the old userID and update their userId fields to the new. Unfortunately it's not working and I can't figure out why.
Here's what my code looks like:
// Cloud Function
exports.transferData = functions.https.onCall((data, context) => {
const firestore = admin.firestore();
const customerQuery = firestore.collection('customers').where('userId', '==', `${data.oldUser}`);
const drinkQuery = firestore.collection('drinks').where('userId', '==', `${data.oldUser}`);
const customerSnapshot = customerQuery.get();
const drinkSnapshot = drinkQuery.get();
const batch = firestore.batch();
for (const documentSnapshot of customerSnapshot.docs) {
batch.update(documentSnapshot.ref, { 'userId': `${data.newUser}` });
};
for (const documentSnapshot of drinkSnapshot.docs) {
batch.update(documentSnapshot.ref, { 'userId': `${data.newUser}` });
};
return batch.commit();
});
// Call from app
func transferData(from oldUser: String, to newUser: String) {
let functions = Functions.functions()
functions.httpsCallable("transferData").call(["oldUser": oldUser, "newUser": newUser]) { _, error in
if let error = error as NSError? {
if error.domain == FunctionsErrorDomain {
let code = FunctionsErrorCode(rawValue: error.code)
let message = error.localizedDescription
let details = error.userInfo[FunctionsErrorDetailsKey]
print(code)
print(message)
print(details)
}
}
}
}
This is the error message from the Cloud Functions log:
Unhandled error TypeError: customerSnapshot.docs is not iterable
at /workspace/index.js:22:51
at fixedLen (/workspace/node_modules/firebase-functions/lib/providers/https.js:66:41)
at /workspace/node_modules/firebase-functions/lib/common/providers/https.js:385:32
at processTicksAndRejections (internal/process/task_queues.js:95:5)
From what I understand customerSnapshot is something called a Promise which I'm guessing is why I can't iterate over it. By now I'm in way too deep for my sparse knowledge and don't know how to handle these Promises returned by the queries.
I guess I could just force users to create a login before they subscribe but that feels like a cowards way out now that I've come this far. I'd rather have both options available and make a decision instead of going down a forced path. Plus, I'll learn some more JavaScript if I figure this out!
Any and all help is greatly appreciated!
EDIT:
Solution:
// Cloud Function
exports.transferData = functions.https.onCall(async(data, context) => {
const firestore = admin.firestore();
const customerQuery = firestore.collection('customers').where('userId', '==', `${data.oldUser}`);
const drinkQuery = firestore.collection('drinks').where('userId', '==', `${data.oldUser}`);
const customerSnapshot = await customerQuery.get();
const drinkSnapshot = await drinkQuery.get();
const batch = firestore.batch();
for (const documentSnapshot of customerSnapshot.docs.concat(drinkSnapshot.docs)) {
batch.update(documentSnapshot.ref, { 'userId': `${data.newUser}` });
};
return batch.commit();
});
As you already guessed, the call customerQuery.get() returns a promise.
In order to understand what you need, you should first get familiar with the concept of promises here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
For your use case, you will probably end up with either using the then callback:
customerQuery.get().then((result) => {
// now you can access the result
}
or by making the method call synchronous, by using the await statement:
const result = await customerQuery.get()
// now you can access the result
Document Reference is used to get document field and its collections from firestore. Following are some examples:
1. Function to read data from field which is having docRef
[Firestore schema]: https://i.stack.imgur.com/pEPK5.png
Here in collection people there is doc named user1 which have a field named hlpr which have docRef for user2 so if i want to access that docRef data i will use following code:
function foo4() {
var user1 = db.collection('people').doc('user1')
user1.get().then((data) => { //get user1 whole doc using get()
var t = data.get('hlpr') //choose field 'hlpr'
t.get().then((doc) => { //get 'hlpr' data() from get()
var user2 = doc.data() //get whole doc from user2
console.log(user2.name) //output field 'name' from user 2
})
})}
2. Read data from array from of docRef. In previous image you can see field named 'cts' which is having array of docRef. Code:
function foo3() { //function to get data from array of docRef
var user1 = db.collection('people').doc('user1'); //choose user1 doc
user1.get().then((doc) => { //get data() of user1 using get()
var contacts = doc.get('cts'); //set var k to field 'cts'
contacts.forEach((data) => { //for each item in array field cts
var userx = data.get(); //read data() using get()
userx.then((doc) => {
var frnd = doc.data(); //set frnd to each data we get from doc
console.log(frnd.name) //output their any field value here i have chosen name
});
});
})}
NOTE: Above code works correctly and you can also use data to put into array also above code might not be best way to get data, but i am a beginner so this is the best i could do.
You can break your code down to a chain of awaits per the comments but a promise chain if processing one item can be fairly clean.
async function foo4() {
var user2 = await db.collection('people').doc('user1').get()
.then(data => data.get('hlpr'))
.then(doc=> doc.data())
.finally(result => result)
.catch(console.log());
you can do some clean code when you nest promise chains.
The second code block has potential errors, if your client exceeds pending 50 documents the client modules will throw errors and fail reads.
I'm trying to retrieve a single document by a field value and then update a field inside it.
When I do .where("uberId", "==",'1234567'), I am getting all the docs with field uberId that matches 1234567.
I know for sure there is only one such document. However, I don't want to use uberId as the document's ID, otherwise I could easily search for the document by ID. Is there another way to search for a single document by a field ID?
So far, reading the docs, I could see this:
const collectionRef = this.db.collection("bars");
const multipleDocumentsSnapshot = await collectionRef.where("uberId", "==",'1234567').get();
Then I suppose I could do const documentSnapshot = documentsSnapshot.docs[0] to get the only existing document ref.
But then I want to update the document with this:
documentSnapshot.set({
happy: true
}, { merge: true })
I'm getting an error Property 'set' does not exist on type 'QueryDocumentSnapshot<DocumentData>'
While you may know for a fact there's only one document with the given uberId value, there is no way for the API to know that. So the API returns the same type for any query: a QuerySnapshot. You will need to loop over the results in that snapshot to get your document. Even when there's only one document, you'll need that loop:
const querySnapshot = await collectionRef.where("uberId", "==",'1234567').get();
querySnapshot.forEach((doc) => {
doc.ref.set(({
happy: true
}, { merge: true })
});
What's missing in your code is the .ref: you can't update a DocumentSnapshot/QueryDocumentSnapshot as it's just a local copy of the data from the database. So you need to call ref on it to get the reference to that document in the database.
async function getUserByEmail(email) {
// Make the initial query
const query = await db.collection('users').where('email', '==', email).get();
if (!query.empty) {
const snapshot = query.docs[0];
const data = snapshot.data();
} else {
// not found
}
}
I writed this method which should console.log the trigered node data, but i get error.
This is what I tried"
exports.makeUppercase = functions.database
.ref('/users/{userId}/matches')
.onWrite((snapshot, context) => {
// Grab the current value of what was written to the Realtime Database.
//const original = snapshot.val();
console.log('OnWrite works' + snapshot.after.val());
// const uppercase = original.toUpperCase();
// You must return a Promise when performing asynchronous tasks inside a Functions such as
// writing to the Firebase Realtime Database.
// Setting an "uppercase" sibling in the Realtime Database returns a Promise.
return null;
});
This is the error:
makeUppercase
TypeError: snapshot.val is not a function at exports.makeUppercase.functions.database.ref.onWrite (/srv/index.js:49:44) at cloudFunction (/srv/node_modules/firebase-functions/lib/cloud-functions.js:131:23) at /worker/worker.js:825:24 at at process._tickDomainCallback (internal/process/next_tick.js:229:7)
Did I made something wrong?
From the docs:
Event data now a DataSnapshot.
In earlier releases, event.data was a DeltaSnapshot; from v 1.0 onward it is a DataSnapshot.
For onWrite and onUpdate events, the data parameter has before and after fields. Each of these is a DataSnapshot with the same methods available in admin.database.DataSnapshot.
For example:
exports.dbWrite = functions.database.ref('/path').onWrite((change, context) => {
const beforeData = change.before.val(); // data before the write
const afterData = change.after.val(); // data after the write
});
Therefore in your code, you need to either use the after property to retrieve the after the write or before property:
const original = snapshot.after.val();
I don't know why mongo mlab _id is not a string? I need to double check context and the viewer._id in my schema. This is my code:
resolve: async ({_id}, {status, ...args}, context) => {
// {_id} destructure _id property on root
console.log("allTodosByUser field = ",_id)
console.log("allTodosByUser field = ",context.user._id)
console.log("allTodosByUser equal",Boolean(_id.toString() === context.user._id.toString())) // suddenly using toString becomes true
This is not really a big deal but somehow I don't want to use toString for comparison:
if(_id.toString() === context.user._id.toString())
So I want to make a function maybe like this:
const { _id, context.user._id: contextUserId } = [_id, context.user._id], // push the _id, and context.user._id in an object so I can destructure?