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