How To Partially Update A Document in Cloud Firestore

Firebase

There will be a scenario where you will want to partially update a document on the Cloud Firestore without compromising security rules.

Let’s say you have an orders collection and a signed-in user with a user role that can create a new order document in that collection, like in the screenshot below.

Recommended
Firebase Cloud Firestore β€” Add, Update, Get, Delete, OrderBy, Limit

User Role

The READ and WRITE security rules to access the orders collection for a user would be like this:

WRITE RULE

match /orders/{ordersId} {
      allow write: if
           request.auth.uid != null && request.auth.token.isUser == true
}

The above security rule will allow a user to create a new document when he/she is logged in and the user role is isUser.

At this stage, you may wonder where the isUser role coming from?

There are a couple of ways to create user roles in Firebase. I use Auth Claims to create roles via Cloud Functions.

For more information on this topic, take a look at my other article that covers in-depth on how to create user roles using Auth Claims.

Recommended
Firebase + Vue.js ← Role Based Authentication & Authorization

READ RULE for a user to access his/her own orders not others.

match /orders/{ordersId} {
      allow write: if
           request.auth.uid == resource.data.user.uid && request.auth.token.isUser == true
}

The above security rule will allow users to get orders when the logged-in user’s uid matches the uid which is inside the user field in the order document like in the screenshot below.

The security rule also checks if the logged-in user has an isUser role.

That’s pretty straight forward.

Driver Role

As you can see from the order image below, I’ve assigned a driver to the order as soon as it’s created. I did it this way for demonstration purposes.

In the real world, you’ll need to assign a driver after the order is placed via the Admin panel or by sending an order notification to the available drivers to accept the order before the order is placed, then add it to the order document.

When a driver is assigned to the order, he/she needs to access certain information in the order, such as the store name, store address, user address, etc.

So let’s give the READ access to the order that the driver is assigned to.

match /orders/{ordersId} {
      allow read: if
           request.auth.uid == resource.data.driver.uid && request.auth.token.isDriver == true
}

That’s good.

Now the user can READ and WRITE his/her own order and the driver can only READ the order document.

Nice.

Recommended
Firebase + Vue.js ← Role Based Authentication & Authorization

Order Status

Now, I want to provide the order status to the user as the driver progresses with his/her order, such as food picked, delivered, etc.

It can be easily done by adding a new field called orderStatus to the order document.

To add the order status to the order document, a driver needs the write permission to that document so the order status can be changed by a driver as he/she progresses with it.

match /orders/{ordersId} {
      allow write: if
           request.auth.uid == resource.data.driver.uid && request.auth.token.isDriver == true
}

That works… right?

At this stage, the driver could possibly not only change the order status but also other information such as order items, addresses, and so on, as he/she has the write permission.

That’s a big issue.

Let’s revoke just the WRITE permission to the driver from the order that he/she is assigned to.

Whew!

Now the driver cannot change any information in the order including the order status.

But, how would a user see the status when a driver does not have permission to change the order status?

Well..

Sub collection is to the rescue.

Adding a sub-collection to an order document allows us to split the data that can be queried separately, as well as have different permissions than the order document that the sub-collection belongs to.

OrderStatus Sub-Collection

Let’s create an orderStatus sub-collection when an order status changes by a driver such as food picked, delivered, etc.

Now, we need to grant read and write permission to the order status collection for a driver. This will make it so that the driver can easily update the order status as the order progress changes.

match /orders/{docId}/orderStatus/{orderStatusId} {
        allow write, read: if request.auth.token.isDriver == true
    }

At this stage the user is unable to access the order status data.

To show the order status data to a user, grant READ permission to the user of the orderStatus collection.

match /orders/{docId}/orderStatus/{orderStatusId} {
   allow read: if request.auth.token.isUser == true
}

In order to get the order data as well as the order status data, we need to make two queries – one to get the actual order document and another one for the orderStatus sub-collection.

let order = {};

try {
  let orderRef = firebase
    .firestore()
    .collection("orders")
    .doc({ orderId });
  let orderSnap = await orderRef.get();
  order = orderSnap.data();
  let orderStatusSnap = await orderRef
    .collection("orderStatus")
    .doc("currentStatus")
    .get();
  order.orderStatus = orderStatusSnap.data();
} catch (e) {
  console.log(e.message);
}

Then, merge them into a single order object on the client-side.

That works!

As you can see, I’ve split the order document data to have different access control.

This way the user can read and write order document data, but the driver can only read the data.

Similarly, the driver can read and write the orderStatus collection document data but the user can only read the data.

Compound Query Issues

Let’s say you want to make a firebase query to get only the orders with a specific order status such as order placed or delivered.

It will be difficult to make this query as the order document does not have the order status information in it.

It would be easier if the order document had all the information including the order status data.

One of the ways to solve this issue is by normalizing the order status data, which means that whenever there is a change on the orderStatus document data, we add a copy of that data into the order document.

How do we do that without messing up the permission?

Well…

Cloud Functions to the rescue.

Using cloud functions from the backend, we can create a database trigger function that will listen for any changes on the orderStatus collection document and update that data to an appropriate order document.

exports.updateOrderStatusOnTheOrderDocument = functions.firestore.document('orders/{orderId}/orderStatus/{orderStatusId}')
    .onWrite(async (change, context) => {
    try {
       const orderStatus = {
            orderStatus: change.after.data()
        }
       admin.firestore().collection('orders').doc(context.params.orderId).update(orderStatus)
   }  catch (error) {
       console.log("error updating order status")
    }
})

As you can see, this orderStatus field looks very similar to before but this time the data is being added or updated via Cloud Functions not from the driver directly.

Now you can filter the orders based on the order status with a single query.

How would you handle this situation? I am curious to know any other approach that you have come up with.

There you have it!