Home
>
Simple Firestore Counter Transactions

Simple Firestore Counter Transactions

8 min read
Jonathan Gamble

jdgamble555 on Wednesday, October 26, 2022 (last modified on Monday, February 26, 2024)

Update 2/26/24

This post is out of date, but I'm keeping it in case someone wants to use the code. I'm not actively working on this package, but you can easily build your own version. See my Four Ways to Count in Firestore post for all the ways to build your own counter.


Original Post

I created two functions similar to Firestore's set and delete, except that they automatically batch counter functions.

First, install the package:

	npm i j-firebase

Set and Delete a Document

	import { setWithCounter, deleteWithCounter } from 'j-firebase';
...
await setWithCounter(
  docRef,
  { somedata: 'somevalue' }
);

The values are just like the regular set function, except it creates a new document in the counters collection. So in this case, if the document reference is in the posts collection, it will also create and increment a counter variable in the _counters/posts collection called count. It will use the name of the target collection under-the-hood, to create a sub collection in the _counters collection.

If you delete a document, the count value will be decremented automatically as well.

	await deleteWithCounter(docRef);

Read the Counter

You can get the total number of posts documents by reading the value of count in the _counters/posts document.

Add Other Counters

I also added the ability to count other documents in batch as well. Let's say you want to get the number of documents by a specific user. You would need to increase the user's post count, the total posts count, and some other specific count. The number of posts a user has may not be the same number as the total number of posts. You could do this with something like this:

	import { setWithCounter, deleteWithCounter } from 'j-firebase';

await setWithCounter(
  docRef,
  data,
  { paths: { users: authorId } }
);

As you can see from the source code, you can input as many paths as you like. Now you will have your user document count on the user's document as postsCount. This is generated from the origin collection name posts plus Count.

Of course, deleting is the same:

	await deleteWithCounter(
  docRef,
  data,
  { paths: { users: authorId } }
);

Security Rules

Here are some copy and paste rules you can re-use.

	function counter() {
  let docPath = 
/databases/$(database)/documents/_counters/$(request.path[3]);
  let afterCount = getAfter(docPath).data.count;
  let beforeCount = get(docPath).data.count;
  let addCount = afterCount == beforeCount + 1;
  let subCount = afterCount == beforeCount - 1;
  let newId = getAfter(docPath).data.docId == request.path[4];
  let deleteDoc = request.method == 'delete';
  let createDoc = request.method == 'create';
  return (newId && subCount && deleteDoc) 
  || (newId && addCount && createDoc);
}

function counterDoc() {
  let doc = request.path[4];
  let docId = request.resource.data.docId;
  let afterCount = request.resource.data.count;
  let beforeCount = resource.data.count;
  let docPath = /databases/$(database)/documents/$(doc)/$(docId);
  let createIdDoc = existsAfter(docPath) && !exists(docPath);
  let deleteIdDoc = !existsAfter(docPath) && exists(docPath);
  let addCount = afterCount == beforeCount + 1;
  let subCount = afterCount == beforeCount - 1;
  return (createIdDoc && addCount) || (deleteIdDoc && subCount);
}

Don't get overwhelmed looking at these functions. The usage is actually quite simple:

	match /posts/{document} {
  allow read;
  allow update;
  allow write: if counter();
}
match /_counters/{document} {
  allow read;
  allow write: if counterDoc();
}

It will check for the corresponding increment and decrement when a document is created or deleted. When a document is incremented or decremented, it will check that the corresponding document is deleted or created. In order to accomplish this, the latest docID will also be temporarily saved on the counter document. This will get overwritten on the next transaction. You really don't need to think about any of this, as it is done automatically.

Here is an example for adding a postsCount on a user document as well:

	function userCount() {
  let colId = request.path[3];
  let docPath = 
/databases/$(database)/documents/users/$(request.auth.uid);
  let beforeCount = get(docPath).data[colId + 'Count'];
  let afterCount = getAfter(docPath).data[colId + 'Count'];
  let addCount = afterCount == beforeCount + 1;
  let subCount = beforeCount == beforeCount - 1;
  return (addCount && request.method == 'create')
  || (subCount && request.method == 'delete');    
}
match /posts/{document} {
  allow read;
  allow update;
  allow create: if counter() && userCount(); 
  allow delete: if counter() && userCount();
}

Dates

You can also pass in dates as true in order to handle createdAt and updatedAt. See Handling Firestore Timestamps.

Options

	export async function setWithCounter<T>(
    ref: DocumentReference<T>,
    data: PartialWithFieldValue<T>,
    setOptions?: SetOptions,
    opts?: {
        paths?: { [col: string]: string },
        dates?: boolean,
    }
)

Questions

The package also has a search index.
If you have questions or feedback, feel free to create an issue.

J


Related Posts

© 2024 Code.Build