I'm running a very simple Firestore transaction which checks for the presence of a document, before writing to it if absent.
(The use case is registering a username - if it's not already registered, the current user gets to grab it)
Here's a snippet of the relevant Flutter code:
DocumentReference usernameDocRef =
Firestore.instance.collection(_USERNAMES).document(username);
await Firestore.instance.runTransaction((transaction) async {
var snapshot = await transaction.get(usernameDocRef);
if (!snapshot.exists) {
transaction.set(usernameDocRef, {
_UsernamesKey.userid: _user.id,
});
}
});
This is failing with an exception "Transaction failed all retries".
Based on the Firestore documentation, failure can occur for two reasons:
The transaction contains read operations after write operations. Read operations must always come before any write operations.
The transaction read a document that was modified outside of the transaction. In this case, the transaction automatically runs again. The transaction is retried a finite number of times.
I don't think I trigger either of those. Any suggestions?
The example transaction in the documentation uses await on its call to update. Perhaps you need the same on your call to set:
await Firestore.instance.runTransaction((transaction) async {
var snapshot = await transaction.get(usernameDocRef);
if (!snapshot.exists) {
await transaction.set(usernameDocRef, {
_UsernamesKey.userid: _user.id,
});
}
});
Firstly, try using the reference from the fresh snapshot and not from the original document reference. If this doesn't work, try changing [set] to [update] as I remember having the same error as you have experience now.
DocumentReference usernameDocRef =
Firestore.instance.collection(_USERNAMES).document(username);
await Firestore.instance.runTransaction((transaction) async {
var snapshot = await transaction.get(usernameDocRef);
if (!snapshot.exists) {
await transaction.update(snapshot.reference, {
_UsernamesKey.userid: _user.id,
});
}
});
This has recently been fixed - https://github.com/flutter/plugins/pull/1206.
If you are using the master channel, the fix should be available already. For the other channels (dev, beta, stable) YMMV.
I'm not a Flutter/Dart expert, but I expect you have to return something from within the transaction, so that Firestore knows when you're done:
await Firestore.instance.runTransaction((transaction) async {
var snapshot = await transaction.get(usernameDocRef);
if (!snapshot.exists) {
return transaction.set(usernameDocRef, {
_UsernamesKey.userid: _user.id,
});
}
})
Related
I'm trying to run a query that retrieves a single row given a where clause and updates it. I understand that Firebase doesn't support an UpdateWhere operations so I'm trying to use a Transaction instead.
I'm having difficulty making it work, maybe I'm too used to sql dbs... Here's my broken code
try {
final whereQuery = _db
.doc(userPath(user))
.collection("someInnerCollection")
.where("active", isEqualTo: true)
.limit(1);
await _db.runTransaction((transaction) async {
final entry = await transaction.get(whereQuery); // This doesn't compile as .get doesn't take in a query
await transaction.update(entry, {
"someValue": "newValue",
});
});
} catch (e) {
...
}
From the test I’ve made, I would suggest the following to achieve what you mention:
Based on the following answer:
As you can see from the API documentation, where() returns a Query object. It's not a DocumentReference.
Even if you think that a query will only return one document, you still have to write code to deal with the fact that it could return zero or more documents in a QuerySnapshot object. I suggest reviewing the documentation on queries to see examples.
After doing the query consult, you have to get the DocumentReference for that given result.
Then, you can use that reference to update the field inside a Batched writes
try {
final post = await firestore
.collection('someInnerCollection')
.where('active', isEqualTo: true)
.limit(1)
.get()
.then((QuerySnapshot snapshot) {
//Here we get the document reference and return to the post variable.
return snapshot.docs[0].reference;
});
var batch = firestore.batch();
//Updates the field value, using post as document reference
batch.update(post, { 'someValue': 'newValue' });
batch.commit();
} catch (e) {
print(e);
}
You are passing the DocumentSnapshot back in the update() operation instead of DocumentReference itself. Try refactoring the like this:
final docRefToUpdate = _db.collection("colName").doc("docId");
await _db.runTransaction((transaction) async {
final entry = await transaction.get() // <-- DocRef of document to update in get() here
await transaction.update(docRefToUpdate, {
// Pass the DocumentReference here ^^
"someValue": "newValue",
});
});
You can use a collection reference and then update single fields using .update().
final CollectionReference collectionReference = FirebaseFirestore.instance.collection('users');
await collectionReference.doc(user.uid).collection('yourNewCollection').doc('yourDocumentInsideNestedCollection').update({
'singleField': 'whatever you want,
});
Same code using "where"
collectionReference.doc(user.uid).collection('yourNewCollection').doc().where('singleField', isEqualTo: yourValue).update({
'singleField': 'whatever you want,
});
If there are more codes/logic outside the transaction that should only be run when the transaction succeeds, will these codes be ran after the retry succeed? See construed example below based on my Express route handler.
app.post('/some/path/to/endpoint', async (req, res) => {
try {
await db.runTransaction(async t => {
const snapshot = t.get(someDocRef);
const data = snapshot.data();
doSomething(snapshot);
return t.update(snapshot.ref, { someChanges });
});
// QUESTION: If transaction retries and succeeds, will the below code run once?
// logic that requires the transaction succeeds
await axios.post(url, data);
res.status(200).send('success');
} catch (e) {
res.status(500).send('system error');
}
});
Appreciate expert views on this. Thanks
You can find the documentation for runTransaction here.
As you can see, runTransaction() returns a Promise. When you await a Promise, and with you code inserted in a try/catch block, if an error is thrown everything after will be ignored, because the flow will go in the catch statement.
So the answer is yes: everything after runTransaction(), the way you wrote it, will not be executed if something goes wrong.
I did a function a few months ago where my application is waiting for the user documents and responding accordingly. It was working like a charm until I optimized and updated the project to the last version.
If there is a user document, the stream yields the document and closes the stream.
If there is no user data in the cloud firestore, the stream yields null and awaits for the document to appear in the cloud.
// this function doesn't work properly and it should work but `firstWhere` is not
// listening to the stream unless there is a listener already which makes no sense
static Stream<DocumentSnapshot> get getUserDocumentWhenExists async* {
User user = FirebaseAuth.instance.currentUser;
if (user == null) throw 'No user is signed in';
FirebaseFirestore firebase = FirebaseFirestore.instance;
CollectionReference usersCollection = firebase.collection('users');
Stream<DocumentSnapshot> userDocumentStream = usersCollection.doc(user.uid).snapshots();
userDocumentStream.listen((event) {}); // <= this is here for the code to work
DocumentSnapshot userDocument = await userDocumentStream.first;
if (userDocument.exists == false) {
yield null;
yield await userDocumentStream.firstWhere((userDocument) {
// not beeing called without a previous listener
return userDocument.exists;
});
} else {
yield userDocument;
}
}
If you run this code without removing userDocumentStream.listen((event) {}) it will work without a problem as it did before the update.
My questions are:
Is this a bug?
Why is this happening? or did I just wrote something wrong?
Edit: I made a custom test without firebase and everything works fine. Just in this particular case firstWhere() is not listening to the stream
Edit2: after some more testing I discovered that any listener after userDocumentStream.first will not work. Now I'm more confused and I really need some help
I think after first() is called, the subscription is canceled even if in the first documentation it says otherwise:
"Internally the method cancels its subscription after the first element. This means that single-subscription (non-broadcast) streams are closed and cannot be reused after a call to this getter."
My solution:
Create a snapshots() stream for the first() and one for firstWhere()
final documentReference = usersCollection.doc(user.uid);
final userDocument = await documentReference.snapshots().first;
if (userDocument.exists == false) {
yield null;
yield await documentReference.snapshots().firstWhere((userDocument) {
return userDocument.exists;
});
} else {
yield userDocument;
}
I'm using firebase functions and I have a function which add new collection when user is creating. The problem is sometimes user is logged in before function is done, so user is logged in but new collection is not created yet (and then I have error message 'Missing or insufficient permissions. because a rule cannot find that collection'). How can I handle it?
Is it possible to finish login user (for example using google provider) only when all stuff from
export const createCollection = functions.auth.user().onCreate(async user => {
try {
const addLanguages = await addFirst();
const addSecondCollection = await addSecond();
async function addFirst() {
const userRef = admin.firestore().doc(`languages/${user.uid}`);
await userRef.set(
{
language: null
},
{ merge: true }
);
return 'done';
}
async function addSecond() {
// ...
}
return await Promise.all([addLanguages, addSecondCollection]);
} catch (error) {
throw new functions.https.HttpsError('unknown', error);
}
});
is finished? So google provider window is closed and user is logged in only after that? (and don't using setTimeouts etc)
AFAIK it is not possible to directly couple the two processes implied in your application:
On one hand you have the Google sign-in flow implemented in your front-end (even if there is a call to the Auth service in the back-end), and;
On the other hand you have the Cloud Function that is executed in the back-end.
The problem you encounter comes from the fact that as soon as the Google sign-in flow is successful, your user is signed in to your app and tries to read the document to be created by the Cloud Function.
In some cases (due for example to the Cloud Function cold start) this document is not yet created when the user is signed in, resulting in an error.
One possible solution would be to set a Firestore listener in your front-end to wait for this document to be created, as follows. Note that the following code only takes into account the Firestore document created by the addFirst() function, since you don't give any details on the second document to be created through addSecond().
firebase.auth().signInWithPopup(provider)
.then(function(result) {
var token = result.credential.accessToken;
var user = result.user;
//Here we know the userId then we can set a listener to the doc languages/${user.uid}
firebase.firestore().collection("languages").doc(user.uid)
.onSnapshot(function(doc) {
if(doc.exists) {
console.log("Current data: ", doc.data());
//Do whatever you want with the user doc
} else {
console.log("Language document not yet created by the Cloud Function");
}
});
}).catch(function(error) {
var errorCode = error.code;
var errorMessage = error.message;
var email = error.email;
var credential = error.credential;
// ...
});
As said above, in the above code we only take into account the first Firestore document created by the addFirst() function. But you probably need to wait for the two docs to be created before reading them from the front-end.
So, you may modify you CF as follows:
export const createCollection = functions.auth.user().onCreate(async user => {
try {
await addFirst();
await addSecond();
return null;
async function addFirst() {
const userRef = admin.firestore().doc(`languages/${user.uid}`);
await userRef.set(
{
language: null
},
{ merge: true }
);
}
async function addSecond() {
// ...
}
} catch (error) {
console.log(error);
return null;
}
});
Note that you don't need to use Promise.all(): the following two lines already execute the two document writes to Firestore. And, since you use async/await the second document is only written after the first one is written.
const addLanguages = await addFirst();
const addSecondCollection = await addSecond();
So you just need to set the listener on the path of the second document, and you are done!
Finally, note that doing
throw new functions.https.HttpsError('unknown', error);
in your catch block is the way you should handle errors for a Callable Cloud Function. Here, you are writing a background triggered Cloud Function, and you can just use return null;
I'm a total newbie to Flutter and I'm trying to add some data from Cloud Firestore to a list in Flutter, but having issues. I try to add the element, but after executing, the element isn't there. It's not throwing an exception or anything either. Maybe someone else has some advice for me!
I have tried changing the type of list (capture the doc from Cloud Firestore instead of data within the doc, same issue), I have also debugPrinted the data I am trying to store to make sure it exists, it does. I have done basic troubleshooting like running flutter clean as well. I am on the latest version of Flutter.
Firestore db = firestore();
List<String> getString() {
var dataList = new List<String>();
db.collection('Users').get().then((querySnapshot) {
querySnapshot.forEach((doc) {
dataList.add(doc.get('First name'));
});
});
debugPrint(dataList.first);
return dataList;
The list is empty, though it should contain the "First name" field on this Cloud Firestore doc. Again, verified the data does exist and prints when calling debugPrint.
The db.collection('Users').get() is a async function, so debugPrint(dataList.first); executes before of the end of your firestores get, because that your array returns empty.
If you try it:
db.collection('Users').get().then((querySnapshot) {
querySnapshot.forEach((doc) {
dataList.add(doc.get('First name'));
});
debugPrint(dataList.first);
});
You will see your data.
You can use await to wait the call finishes, so you must return a Future and use async key word on function declaration. This is a conceipt that you must know of flutter async functions (Async Flutter). So, the code below can solve your problem.
Firestore db = firestore();
Future <List<String>> getString() async {
var dataList = new List<String>();
var result = await db.collection('Users').get();
result.forEach((doc) {
dataList.add(doc.get('First name'));
});
debugPrint(dataList.first);
return dataList;
}