Firestore transactions with security rules making reads - firebase

I want to create two documents
Account/{uid} {
consumerId: ... //client generated id
}
Consumer/{consumerId} {
...
}
and I have a security rule for the consumer collection
match /Consumer/{consumerId} {
allow create: if (consumerId == get(/databases/$(database)/documents/Account/$(request.auth.uid)).data['consumerId'];
}
I need to ensure that an account can only add a consumer document with a consumerId corresponding to the one in their Account document. Both documents should be created together. I've been trying to do this with transactions but I keep getting the error "Transaction failed all retries.". Whats going wrong and how do I fix it?

The data variable is an object and not an array, so you should use data.consumerId instead of data['consumerId']:
match /Consumer/{consumerId} {
allow create: if consumerId == get(/databases/$(database)/documents/Account/$(request.auth.uid)).data.consumerId;
}

I ended up accomplishing this with a batch write and security rules.
match /consumer/{cid} {
function isNewResource() { return resource == null; }
allow create: if isRegistered();
allow read, update: if isNewResource();
}
And then client side with something along the lines of
createThing() {
const db = firebase.firestore();
const { uid, displayName } = this.auth.currentUser;
const batch = this.db.batch();
// Essentially generating a uuid
const newConsumerRef = db.collection("consumer").doc();
// Update the user doc
batch.update(
db.collection('/users').doc(uid),
{ consumerID: newConsuemrRef.id }
);
// Update the admins field in the new doc
batch.set(newConsumerRef, {
admins: {
[uid]: displayName,
},
});
return batch.commit();
}
My problem was the same, but the write to the field in the collections actually needed to be to an object key, so it looked a little funkier
batch.update(
db.collection('/users').doc(uid),
{ [`adminOf.${newRef.id}`]: 'some special name' }
);

Related

Trigger Email Firebase Extension on modified Document

How do i trigger the sending of an email when a document data is modified ?
Trigger Email only composes and sends an email based on the contents of a document written to a Cloud Firestore collection but not modified
I can’t figure this one out…
From checking the code we can see that the extension already processes all writes to the document:
export const processQueue = functions.handler.firestore.document.onWrite(
...
From looking a bit further it seems that the action the extension takes upon a write depends on the value of the delivery.state field in the document.
async function processWrite(change) {
...
const payload = change.after.data() as QueuePayload;
...
switch (payload.delivery.state) {
case "SUCCESS":
case "ERROR":
return null;
case "PROCESSING":
if (payload.delivery.leaseExpireTime.toMillis() < Date.now()) {
// Wrapping in transaction to allow for automatic retries (#48)
return admin.firestore().runTransaction((transaction) => {
transaction.update(change.after.ref, {
"delivery.state": "ERROR",
error: "Message processing lease expired.",
});
return Promise.resolve();
});
}
return null;
case "PENDING":
case "RETRY":
// Wrapping in transaction to allow for automatic retries (#48)
await admin.firestore().runTransaction((transaction) => {
transaction.update(change.after.ref, {
"delivery.state": "PROCESSING",
"delivery.leaseExpireTime": admin.firestore.Timestamp.fromMillis(
Date.now() + 60000
),
});
return Promise.resolve();
});
return deliver(payload, change.after.ref);
}
}
My guess is that if you clear that field, the extension will pick up on that change and try to mail the document again.

Firebase query `TypeError` using `firestore.FieldPath.documentId()`

My Firestore data structure looks like this:
db.firestore.FieldPath.documentId(), '==', '20210106.0' does not work, but I am not sure why. I need to read it as a float, so I can use => or =< as Start Date and End Date in my query.
In the console I get this error message: TypeError: Cannot read property 'FieldPath' of undefined'
Here is my code:
actions: {
getFireBaseOrders(state){
db.collection(`ordersOptimized`).where(
db.firestore.FieldPath.documentId(),
'==',
'20210106.0').onSnapshot((res) => {
const changes = res.docChanges();
changes.forEach((change) => {
if (change.type === "added") {
let payload = change.doc.data();
state.commit("firebaseOrders", payload);
}
});
});
},
What am I missing? How do I make the condition work?
If you want to listen to changes occuring to the Firestore document with ID 20210106.0, just do as follows:
db.collection("ordersOptimized").doc("20210106.0").get()
.onSnapshot(function(doc) {
// ....
// Based on your database screenshot you should loop over the
// JavaScript object returned by doc.data()
// Something like
for (const [key, value] of Object.entries(doc.data())) {
console.log(`${key}: ${value}`);
}
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
});
Since 20210106.0 is the ID of a document in the ordersOptimizedcollection, only one document with this ID can exist in this collection. Therefore you should not use a Query (i.e. db.collection('...').where('...')) in order to listen to changes to this document.
On this other hand, if you want to listen to ALL the documents of the ordersOptimized collection, see the corresponding doc.

Firebase Realtime DB: Order query results by number of values for a key

I have a Firebase web Realtime DB with users, each of whom has a jobs attribute whose value is an object:
{
userid1:
jobs:
guid1: {},
guid2: {},
userid2:
jobs:
guid1: {},
guid2: {},
}
I want to query to get the n users with the most jobs. Is there an orderby trick I can use to order the users by the number of values the given user has in their jobs attribute?
I specifically don't want to store an integer count of the number of jobs each user has because I need to update users' jobs attribute as a part of atomic updates that update other user attributes concurrently and atomically, and I don't believe transactions (like incrementing/decrementing counters) can be a part of those atomic transactions.
Here's an example of the kind of atomic update I'm doing. Note I don't have the user that I'm modifying in memory when I run the following update:
firebase.database().ref('/').update({
[`/users/${user.guid}/pizza`]: true,
[`/users/${user.guid}/jobs/${job.guid}/scheduled`]: true,
})
Any suggestions on patterns that would work with this data would be hugely appreciated!
Realtime Database transactions run on a single node in the JSON tree, so it would be quite difficult to integrate the update of a jobCounter node within your atomic update to several nodes (i.e. to /users/${user.guid}/pizza and /users/${user.guid}/jobs/${job.guid}/scheduled). We would need to update at /users/${user.guid} level and calculate the counter value, etc...
An easier approach is to use a Cloud Function to update a user's jobCounter node each time there is a change to one of the jobs nodes that implies a change in the counter. In other words, if a new job node is added or removed, the counter is updated. If an existing node is only modified, the counter is not updated, since there were no change in the number of jobs.
exports.updateJobsCounter = functions.database.ref('/users/{userId}/jobs')
.onWrite((change, context) => {
if (!change.after.exists()) {
//This is the case when no more jobs exist for this user
const userJobsCounterRef = change.before.ref.parent.child('jobsCounter');
return userJobsCounterRef.transaction(() => {
return 0;
});
} else {
if (!change.before.val()) {
//This is the case when the first job is created
const userJobsCounterRef = change.before.ref.parent.child('jobsCounter');
return userJobsCounterRef.transaction(() => {
return 1;
});
} else {
const valObjBefore = change.before.val();
const valObjAfter = change.after.val();
const nbrJobsBefore = Object.keys(valObjBefore).length;
const nbrJobsAfter = Object.keys(valObjAfter).length;
if (nbrJobsBefore !== nbrJobsAfter) {
//We update the jobsCounter node
const userJobsCounterRef = change.after.ref.parent.child('jobsCounter');
return userJobsCounterRef.transaction(() => {
return nbrJobsAfter;
});
} else {
//No need to update the jobsCounter node
return null;
}
}
}
});

Firestore get value of Field.increment after update without reading the document data

Is there a way to retrieve the updated value of a document field updated using firestore.FieldValue.increment without asking for the document?
var countersRef = db.collection('system').doc('counters');
await countersRef.update({
nextOrderCode: firebase.firestore.FieldValue.increment(1)
});
// Get the updated nextOrderCode without asking for the document data?
This is not cost related, but for reliability. For example if I want to create a code that increases for each order, there is no guaranty that if >= 2 orders happen at the same time, will have different codes if I read the incremental value right after the doc update resolves, because if >= 2 writes happen before the first read, then at least 2 docs will have the same code even if the nextOrderCode will have proper advance increment.
Update
Possible now, check other answer.
It's not possible. You will have to read the document after the update if you want to know the value.
If you need to control the value of the number to prevent it from being invalid, you will have to use a transaction instead to make sure that the increment will not write an invalid value. FieldValue.increment() would not be a good choice for this case.
We can do it by using Firestore Transactions, like incremental worked before Field.increment feature:
try {
const orderCodesRef = admin.firestore().doc('system/counters/order/codes');
let orderCode = null;
await admin.firestore().runTransaction(async transaction => {
const orderCodesDoc = await transaction.get(orderCodesRef);
if(!orderCodesDoc.exists) {
throw { reason: 'no-order-codes-doc' };
}
let { next } = orderCodesDoc.data();
orderCode = next++;
transaction.update(orderCodesRef, { next });
});
if(orderCode !== null) {
newOrder.code = orderCode;
const orderRef = await admin.firestore().collection('orders').add(newOrder);
return success({ orderId: orderRef.id });
} else {
return fail('no-order-code-result');
}
} catch(error) {
console.error('commitOrder::ERROR', error);
throw errors.CantWriteDatabase({ error });
}
Had the same question and looks like Firestore Python client
doc_ref.update() returns WriteResult that has transform_results attribute with the updated field value

How do you change the email associated with a user in firebase simple login?

As the title suggests I would like to provide functionality to allow a user to update the email they use to login to my app using Firebase Simple Login. Cannot figure out an elegant way to do this. App uses AngularFire if that is relevant.
Does one exist or do I need to create a new account and delete the old one using the $removeUser() and $createUser() methods?
Update for Firebase 2.1.x
The Firebase SDK now provides a changeEmail method.
var ref = new Firebase('https://<instance>.firebaseio.com');
ref.changeEmail({
oldEmail: 'kato#domain.com',
newEmail: 'kato2#kato.com' ,
password: '******'
}, function(err) {
console.log(err ? 'failed to change email: ' + err : 'changed email successfully!');
});
Historical answer for Firebase 1.x
In Simple Login, this is equivalent to changing the user's ID. So there is no way to do this on the fly. Simply create the new account, remove the old one as you have already suggested.
If you're user profiles in Firebase, you'll want to move those as well. Here's brute force, safe method to migrate an account, including user profiles. You could, naturally, improve upon this with some objectification and futures:
var ref = new Firebase('URL/user_profiles');
var auth = new FirebaseSimpleLogin(ref);
// assume user has logged in, or we obtained their old UID by
// looking up email address in our profiles
var oldUid = 'simplelogin:123';
moveUser( auth, ref, oldUid, '123#abc.com', '456#def.com', 'xxx' );
function moveUser( auth, usersRef, oldUid, oldId, newId, pass ) {
// execute activities in order; first we copy old paths to new
createLogin(function(user) {
copyProfile(user.uid, function() {
// and once they safely exist, then we can delete the old ones
removeOldProfile();
removeOldLogin();
});
});
function copyProfile(newId, next) {
ref.child(oldUid).once('value', function(snap) {
if( snap.val() !== null ) {
ref.child(newId, snap.val(), function(err) {
logError(err);
if( !err ) { next(); }
});
}
});
}
function removeOldProfile() {
ref.child(oldId).remove(logError);
}
function createLogin(next) {
auth.createUser(newId, pass, function(err, user) {
logError(err);
if( !err ) { next(user); }
});
}
function removeOldLogin() {
auth.removeUser(oldId, pass, logError);
}
}
function logError(err) {
if( err ) { console.error(err); }
}

Resources