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

@forest-fire/firebase-rules

v1.1.2

Published

A growing fork of the fantastic rules from Geoges Boris

Downloads

5

Readme

Firebase Rules

Make your Firebase database rules readable and scalable. Using only typescript/javascript.

This library was forked from firebase-toolbelt/firebase-rules and converted to Typescript. Subtle parts of the API have been improved to allow for greater flexibility. Also, most of the exposed API is now provided as function rather than just static constants.

Table of Contents

Getting Started

The best way to understand how it works it's just by using it. Let's try it out in this quick demo. Let's create some rules for an app that let users create their own 'user' object and then create/update their own posts.

The database should look something like this:

{
  "users": {
    "$userId": {
      "firstName": "John"
    }
  },
  "posts": {
    "$postId": {
      "title": "My post title.",
      "body": "My post body.",
      "createdAt": "timestamp",
      "createdBy": "$userId"
    }
  }
}

Let's first create our rules related to the user object. The user must be able to create and update his profile. Other users must be able to see his profile. No users are ever removed from our app.

// ./rules/modules/users.js

import {
  isAuth,
  isAuthId,
  newDataExists,
  hasChildren,
  isString,
  validate,
  ifCondition
} from 'firebase-rules';

const isUserAndIsNotRemoving = ifCondition(
  newDataExists(),
  isAuthId('$userId'),
  false
);

export default {
  'users/$userId': {
    read: isAuth,
    write: isUserAndIsNotRemoving,
    validate: hasChildren('firstName')
  },
  'users/$userId/firstName': validate(isString),
  'users/$userId/$invalidProp': validate(false)
});

Next we can build our post object rules. Any user can create posts. Only the post's creator may update or remove it. Posts can be read by any of our app's users.

// ./rules/modules/posts.js

import {
  isAuth,
  isAuthId,
  isString,
  isNow,
  newProp,
  hasChildren,
  validate
} from 'firebase-rules/helpers/common';

export default {
  'posts/$postId': {
    read: isAuth,
    write: isAuthId(newProp('createdBy')),
    validate: hasChildren('title', 'body', 'createdAt', 'createdBy')
  },
  'posts/$postId/title': validate(isString),
  'posts/$postId/body': validate(isString),
  'posts/$postId/createdAt': validate(isNow),
  'posts/$postId/createdBy': validate(isAuthId(newData)),
  'posts/$postId/$invalidProp': validate(false)
};

Now let's export this rules so we can import them to firebase.

import { createRule } from 'firebase-rules';
import { users, posts } from './modules';

createRules({
  ...users,
  ...posts
}, 'database.rules.json');

Done! There is now a file named database.rules.json in our app's root folder. We can now upload our app to Firebase using their CLI or even upload our file manually using their website.

Helpers

We include many helpers that are commonly used when building a firebase ruleset. Keep in mind that we're only generating strings. So you can quickly create helpers of your own. Check out our helpers source code, they're so simple it's ridiculous.

Conditions

Using our conditions helpers will make writing your code more like writing normal code logic.

ifOrElse

If the first condition is true, the second condition is checked. If the first condition is false, the third condition is checked. It works much like a ternary operator.

If the third condition is not provided, it defaults to 'false'.

ifOrElse(
  ifUserIsAuth,
  alsoCheckIfHeIsValid,
  otherwiseCheckOtherCondition
)

anyCondition

Any of the passed conditions must be true for the rule to be accepted. It might also receive an array instead of a list of arguments.

anyCondition(
  thisMustBeTrue,
  OR_thisMustBeTrue,
  OR_atLeastThisMustBeTrue,
  ...
)

everyCondition

Any of the passed conditions must be true for the rule to be accepted. It might also receive an array instead of a list of arguments.

everyCondition(
  thisMustBeTrue,
  AND_thisTooMustBeTrue,
  AND_thisMustAlsoBeTrue,
  ...
)

CRUD

Our CRUD helpers makes it easy to check for different rules in case of create/update/delete situations.

onCreate( checkIfAllChildrenArePresent ),
onUpdate( checkIfNoRequiredChildrenAreRemoved ),
onDelete( checkIfUserCanDeleteThis )

Note that onDelete is not called on validate rules since firebase bypasses validations on null values.

Authorization

Functions include:

isLoggedIn()
isUser(uid: string)
hasCustomClaim(claim: string)
customClaimValue(claim: string, value: scalar, child?: string)
customClaimContains(claim: string, value: scalar, child?: string)
hasEmail()
emailMatches(regEx: string)
hasVerifiedEmail()
hasPhoneNumber()

So if you wanted to ensure all logged in users can read but you can only write to posts which you own:

{
  'posts/$userId/$postId': {
    read: isLoggedIn(),           // 'auth.uid != null',
    write: isUser('$userId')      // 'auth.uid == $userId'
  }
}

Common

We also provide a lot of common snippets so you won't have to redo the basics.

Data

data(child?: string)
isValue(value: string | number | boolean, childPath?: string)
dataExists(child?: string)
dataDoesNotExist(child?: string)

newData(child?: string)
isNewValue(value: string | number | boolean, childPath?: string)
newDataExists(child?: string)
newDataDoesNotExist(child?: string)

e.g. the path can only be created but not updated

$path:
  write: everyCondition( dataIsEmpty, newDataExists )
  // data.val() == null && newData.val() != null

Props

child(path: string)
newChild(path: string)
hasChildren(...children: string[])

e.g. a post can only be created with all required fields filled. the createdBy must hold the userId.

post:
  write: everyCondition(
    hasChildren('title', 'body', 'createdBy'),
    isAuthId(newProp('createdBy'))
  )
  // newData.hasChildren(['title', 'body', 'createdBy']) && newData.child('createdBy').val() == auth.uid

Validation

validate(conditions)

isString(child?: string)
isNumber(child?: string)
isInteger(child?: string)
isBoolean(child?: string)

isBefore(timestamp: number)
isAfter(timestamp: number)

A lot of paths will only hold validation rules so there's a short-hand function that helps with that.

const rules = {
  "posts/$postId/title":     validate(isString())
  "posts/$postId/likes":     validate(isNumber())
  "posts/$postId/archived":  validate(isBoolean())
  "posts/$postId/createdAt": validate(isBefore(23434364))
  "posts/$postId/createdBy": validate(isUser(data()))
}

Indexing

indexOn(...properties: string[])

e.g. indexing some children for optimized querying

{
  "posts/$postId":
    indexOn: ['createdAt', 'createdBy']
}

User Defined String

s('something') // '\'something\''

Since we're dealing with an object that will be turned to a json, sometimes it's useful escaping user defined strings so they're not mistaken for variables by the firebase rules parser. This is normally used when passing a user defined string to a function like so:

const userExists = (userId: string) => `root.child('users').child(${userId}).exists()`;

userName('$userId') // `root.child('users').child($userId).exists()`
userName('123')     // `root.child('users').child(123).exists()` -> ERROR -> `123` is not a valid variable
userName(s(123))    // `root.child('users').child('123').exists()`

Transformers

toData(input: string | function)
toNewData(input: string | function)

There will be a time you will want to duplicate a rule so it checks both data and newData. This transformers will help you building code without having to duplicate it in these situations.

const userExists = (userId) => `root.child('users').child(${userId}).exists()`;
const userWillExist = toNewData(userExists);

userExists(child('createdBy')) // root.child('users').child(data.child('createdBy').val()).exists()
userWillExist('$userId') // newDataRoot().child(newData.child('createdBy').val()).exists()

See the newDataRoot() that appears on the output above? Read below to understand it better.

New Data Root

Turns out you can't really use the root variable when you're dealing with the data that is being added to your database. This can be a bit of a pain when you're defining reusable rules that will be used on a lot of different paths.

Let me show you what I mean.

{
  "users/$userId/numberOfPosts":
    validate: 'root.child('posts').child('$userId').numChildren() == newData.val()'
}

The above code would work perfectly if you are increasing the user/numberOfPosts prop after you've already created the post on the database. But if you're creating the post at the same time you're updating this counter it would not really work. root can't be used in the context of the data that is being added. To solve this you would have to do something like this:

{
  "users/$userId/numberOfPosts":
    validate: 'newData.parent().parent().child('posts').child('$userId').numChildren() == newData.val()'
}

Messy, right? Not only that, you won't be able to reuse any rule throughout your application because things may be at different depths, so they will require a different number of parent() to be called.

Fear not. We've got your back. While we're parsing your rules, we will check for a special keyword newDataRoot() and we will replace it with the correct code regarding the path you're using it. Let's see it in action.

const numberOfPosts = userId => `newDataRoot().child('posts').child(${userId}).numChildren()`;
const rules = {
  "users/$userId":
    validate: `${numberOfPosts('$userId')} == newData.child('numberOfPosts').val()'
  // 'newData.parent().child('posts').child('$userId').numChildren() == newData.val()'
  
  "users/$userId/numberOfPosts":
    validate: `${numberOfPosts('$userId')} == newData.val()'
  // 'newData.parent().parent().child('posts').child('$userId').numChildren() == newData.val()'
}

This is really useful when you're creating data on multiple locations that depend on each other.

Testing

We recommend using the excelent targaryen library for testing your firebase rules without reaching for the server. Using it with firebase-rules is extremelly easy as you can generate your rules as an object instead of creating them on a file by just omitting the filename when calling the createRules function.

import targaryen from 'targaryen';
const chai, { expect } = require('chai');
chai.use(targaryen);

const createRules = require('firebase-rules');
const { anyCondition } = require('firebase-rules/helpers/conditions');

/**
 * You can retrieve your rules as an object
 * by just omitting the filename argument.
 */
const myFirebaseRules = createRules({
  '/my/path': {
    validate: anyCondition(
      `newData.val() == 'a'`,
      `newData.val() == 'b'`
    )
  }
});

/**
 * an object representing your firebase data
 */
const myFirebaseData = {};

/**
 * Now you can test your rules!
 */

describe('my firebase rules tests', function() {

  before(function() {
    targaryen.setFirebaseData(myFirebaseData);
    targaryen.setFirebaseRules(myFirebaseRules);
  });

  it('should only allow 'a' or 'b' values to be written to `/my/path`', function() {
    expect(null).can.write('a').to.path('/my/path');
    expect(null).cannot.write('c').to.path('/my/path');
  })

});

Made with ♥ by Georges Boris Converted to Typescript by Ken Snyder