sofia

Firestore Rules. With variables.

🤔 What is sofia?

sofia is a representation of Firestore Rules described using JSON, which provides several benefits over the .rules syntax:

Provides variable declarations to reduce verbosity

Promotes more rigid and predictable rules structure

Easily integrated with dynamic representations

Relative path resolution

Intuitive conditions

🚀 Installing

Using npm :

npm install --save @cawfree/sofia

Using yarn :

yarn add @cawfree/sofia

✔️ Getting Started

import sofia , { $ ifel } from ' @cawfree/sofia ' ; const rules = { $userId : ' request.auth.uid ' , ' databases/{database}/documents ' : { ' user/{document=**} ' : { $userIsAuthed : ' $userId != null ' , $exists : { $userIsBlocked : ' ./../../blocked/$($userId) ' , } , $read : ' $userIsAuthed ' , $write : ' $userIsAuthed && !$userIsBlocked ' , } , } , } ; console . log ( sofia ( rules ) ) ;

✍️ Syntax Examples

Simple Variables

In the example below, we provide an example of dynamically constructing a sofia -compatible JSON object.

const ensureNotDeleted = doc => ` ! ${ doc } .deleted ` ; const ensureUserNotChanged = ( next , last ) => ` ${ next } .userId == $userId && ${ next } .userId == ${ last } .userId ` ; const rules = sofia ( { $nextDoc : ' request.resource.data ' , $lastDoc : ' resource.data ' , $userId : ' request.auth.uid ' , $offset : ' request.query.offset ' , [ ' databases/{database}/documents ' ] : { [ ' atomic/{docId} ' ] : { $list : ' $offset == null || $offset == 0 ' , $update : [ ensureNotDeleted ( ' $nextDoc ' ) , ensureUserNotChanged ( ' $nextDoc ' , ' $lastDoc ' ) , ] . join ( ' && ' ) , } , } , } , ) ;

After a call to sofia , the returned .rules are as follows:

service cloud.firestore { match /databases/{database}/documents { match /atomic/{docId} { allow list: if request.query.offset == null || request.query.offset == 0; allow update: if !request.resource.data.deleted && request.resource.data.userId == request.auth.uid && request.resource.data.userId == resource.data.userId; } } }

Transaction Variables

It is also possible to use transaction variables; these permit us to interact with the results of transcions such as exists or getAfter themselves, just as if they were like any other variable. These help clearly establish the relationships that exist between collections.

{ [ ' databases/{database}/documents ' ] : { $nextDoc : ' request.resource.data ' , $userId : ' request.auth.uid ' , [ ' outer/{document=**} ' ] : { $getAfter : { $outerVariable : ' ./$($userId) ' , } , $read : ' $outerVariable != null ' , [ ' inner/{innerRefId} ' ] : { $innerVariable : ' $outerVariable.userId ' , $create : ' $innerVariable == $userId ' , } , } , } , }

After a call to sofia , the returned .rules are as follows:

service cloud.firestore { match /databases/{database}/documents { match /outer/{document=**} { allow read: if getAfter(/databases/$(database)/documents/outer/$(request.auth.uid)) != null; match /inner/{innerRefId} { allow create: if getAfter(/databases/$(database)/documents/outer/$(request.auth.uid)).userId == request.auth.uid; } } } }

Conditions

It is even possible to define conditions. These help clearly define which rules need to be processed based upon a previous condition. Since .rules are predefined, it's probably useful to note that there's nothing special going on here, conditions merely resolve to a lazy evaluation of both the positive and negative outcome, which effectively creates a branch in your static logic.

This block emphasises that sofia can result in more readable rule definitions, when handling more complex transactions.

{ $nextDoc : ' request.resource.data ' , $userId : ' request.auth.uid ' , [ ' databases/{database}/documents ' ] : { [ ' user/{someUserId} ' ] : { $exists : { $friendRecord : ' ./../../friendsList/$(someUserId)/friend/$($userId) ' , } , $read : ' !resource.data.deleted && ' + $ifel ( ' someUserId == $userId ' , ( ) => ' true ' , ( ) => ' $friendRecord ' , ) , } , [ ' friendsList/{someFriendsListId} ' ] : { [ ' friend/{friendId} ' ] : { } , } , } , }

After a call to sofia , the returned .rules are as follows. As you can see, the order of the evaluated conditions are preserved, without the headaches.

service cloud.firestore { match /databases/{database}/documents { match /user/{someUserId} { allow read: if (((!resource.data.deleted) && ((someUserId == request.auth.uid) && true)) || ((!(someUserId == request.auth.uid)) && exists(/databases/$(database)/documents/friendsList/$(someUserId)/friend/$(request.auth.uid)))); } match /friendsList/{someFriendsListId} { match /friend/{friendId} { } } } }

For further information, check out index.test.js to find a complete breakdown of the sofia syntax.

✌️ Credits

Made possible by jsep.