npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@cawfree/sofia

v1.0.35

Published

Firestore rules. With variables.

Downloads

135

Readme

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';

// declare rules json using sofia syntax
const rules = {
  $userId: 'request.auth.uid',
  'databases/{database}/documents': {
    'user/{document=**}': {
      $userIsAuthed: '$userId != null',
      $exists: {
        $userIsBlocked: './../../blocked/$($userId)',
      },
      $read: '$userIsAuthed',
      $write: '$userIsAuthed && !$userIsBlocked',
    },
  },
};

// print the firebase-compatible rules
console.log(sofia(rules));

✍️ Syntax Examples

Simple Variables

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

// Checks whether the referenced document is not deleted.
const ensureNotDeleted = doc => `!${doc}.deleted`;
// Ensures that a document's user information can never change.
const ensureUserNotChanged = (next, last) => `${next}.userId == $userId && ${next}.userId == ${last}.userId`;
const rules = sofia(
  {
    // Define $variables that are scoped to the adjacent collections and their subcollections.
    // Note that variables are subject to by subcollections.
    $nextDoc: 'request.resource.data',
    $lastDoc: 'resource.data',
    $userId: 'request.auth.uid',
    $offset: 'request.query.offset',
    ['databases/{database}/documents']: {
      // Define the reference of the existing collection. This object effectively
      // describes the database root as 'databases/{database}/documents'.
      ['atomic/{docId}']: {
        // Here we define the list rule, where we state callers are permitted
        // to make list queries if they have provided a falsey offset. 
        // Looking at the global variables, offset refers to "request.query.offset".
        $list: '$offset == null || $offset == 0',
        // Here we can execute additional conditions based upon the results of the 
        // function invocations.
        $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=**}']: {
      // Declare a number of $getAfter variables within the scope
      // of the 'outer' collection and its subcollections.
      $getAfter: {
        $outerVariable: './$($userId)',
      },
      $read: '$outerVariable != null',
      ['inner/{innerRefId}']: {
        // It is possible to even parse data out of the result
        // of a transaction from an adjacent cell!
        $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',
        // All users are allowed to read their own documents.
        () => 'true',
        // If another user is trying to get the user information,
        // make sure they are part of their friends first.
        () => '$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.