I've been working on replacing the Kids In Touch API with Firebase. I've struggled a bit with the rules system. It's pretty complicated putting the rules for an entire API in a single, understandable file.

To help with this, Firebase has released their new Blaze compiler. It seems pretty awesome. I've played with it a bit but find it overwhelming for now. I need to move quickly on what I'm doing right now and don't have time to learn something else that is completely new to me.

So, I'm sticking with the default of writing rules in JSON.

If you have a very simple API, then a single "rules.json" file is adequate :

{
  "rules": {
    "room_names": {
      ".read": true,
      "$room_id": {
        ".validate": "newData.isString()"
      }
    },
    "messages": {
      "$room_id": {
        ".read": true,
        ".validate": "root.child('room_names/'+$room_id).exists()",
        "$message_id": {
          ".write": "!data.exists() && newData.exists()",
          ".validate": "newData.hasChildren(['name', 'message', 'timestamp'])",
          "name": {
            ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 20 && !newData.val().contains('admin')"
          },
          "message": {
            ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 50"
          },
          "timestamp": {
            ".validate": "newData.val() <= now"
          },
          "$other": {
            ".validate": false
          }
        }
      }
    }
  }
}

When you start adding data points and "queue" collections, things can get pretty ugly, pretty quickly. Contrived example:

{
  "rules": {
    "room_names": {
      ".read": true,
      "$room_id": {
        ".validate": "newData.isString()"
      }
    },
    "messages1": {
      "$room_id": {
        ".read": true,
        ".validate": "root.child('room_names/'+$room_id).exists()",
        "$message_id": {
          ".write": "!data.exists() && newData.exists()",
          ".validate": "newData.hasChildren(['name', 'message', 'timestamp'])",
          "name": {
            ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 20 && !newData.val().contains('admin')"
          },
          "message": {
            ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 50"
          },
          "timestamp": {
            ".validate": "newData.val() <= now"
          },
          "$other": {
            ".validate": false
          }
        }
      }
    },
    "messages2": {
      "$room_id": {
        ".read": true,
        ".validate": "root.child('room_names/'+$room_id).exists()",
        "$message_id": {
          ".write": "!data.exists() && newData.exists()",
          ".validate": "newData.hasChildren(['name', 'message', 'timestamp'])",
          "name": {
            ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 20 && !newData.val().contains('admin')"
          },
          "message": {
            ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 50"
          },
          "timestamp": {
            ".validate": "newData.val() <= now"
          },
          "$other": {
            ".validate": false
          }
        }
      }
    },
    "messages3": {
      "$room_id": {
        ".read": true,
        ".validate": "root.child('room_names/'+$room_id).exists()",
        "$message_id": {
          ".write": "!data.exists() && newData.exists()",
          ".validate": "newData.hasChildren(['name', 'message', 'timestamp'])",
          "name": {
            ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 20 && !newData.val().contains('admin')"
          },
          "message": {
            ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 50"
          },
          "timestamp": {
            ".validate": "newData.val() <= now"
          },
          "$other": {
            ".validate": false
          }
        }
      }
    },
    "messages3": {
      "$room_id": {
        ".read": true,
        ".validate": "root.child('room_names/'+$room_id).exists()",
        "$message_id": {
          ".write": "!data.exists() && newData.exists()",
          ".validate": "newData.hasChildren(['name', 'message', 'timestamp'])",
          "name": {
            ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 20 && !newData.val().contains('admin')"
          },
          "message": {
            ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 50"
          },
          "timestamp": {
            ".validate": "newData.val() <= now"
          },
          "$other": {
            ".validate": false
          }
        }
      }
    }

  }
}

That was a lot of scrolling, right? It gets pretty hard to read all of that in a single screen. Then, keeping track of versions is a bit of a nightmare. Did you change something in the right spot? Did you change the rules for 2 or more nodes? Worse, is that the "Rules" view in the dashboard can get out of sync with your actual code on disk. Anyone with access to your Dashboard, can go in and change the rules. So, now your rules are not what you think they are.

I put together a simple Gulp process to help me with this.

Goals:

  • Each "node" or collection has it's own JSON file
  • Process compiles all the different node rules files into one "rules-compiled.json" file
  • The output is minified so that anyone going into the dashboard will be less inclined to start mucking around with it.

Working from an idea I got from Jeff French, I've created a simple Gulp task that manages all this. This is based on Jeff's "Environment Variables in AngularJS and Ionic" post.

Rules Template:

{
    "rules": {

        ".write": false,
        ".read": false,

        "messages1" : @@messages1,

        "messages2" : @@messages2,

        "messages3" : @@messages3
    }
}

All Nodes Have Their Own File

Each of the "messagesX" nodes will have their own file like "messages1.json". It contains ONLY the rules for that one node.

{
  "$room_id": {
    ".read": true,
    ".validate": "root.child('room_names/'+$room_id).exists()",
    "$message_id": {
      ".write": "!data.exists() && newData.exists()",
      ".validate": "newData.hasChildren(['name', 'message', 'timestamp'])",
      "name": {
        ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 20 && !newData.val().contains('admin')"
      },
      "message": {
        ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 50"
      },
      "timestamp": {
        ".validate": "newData.val() <= now"
      },
      "$other": {
        ".validate": false
      }
    }
  }
}

Simple Gulp Process Creates rules-compiled.json

The gulpfile.js:

var gulp       = require('gulp');
var replace    = require('gulp-replace-task');
var concat     = require('gulp-concat');
var fs         = require('fs');
var jsonminify = require('gulp-jsonminify');

gulp.task('default', function () {

    // Read the individual rules files
    var messages1 = fs.readFileSync('./rules/messages1.json', 'utf8');
    var messages2 = fs.readFileSync('./rules/messages2.json', 'utf8');
    var messages3 = fs.readFileSync('./rules/messages3.json', 'utf8');

    // Replace the patterns from the individual rules files
    gulp.src('./rules/rules-template.json')
        .pipe(replace({
            patterns: [
                {
                    match: 'messages1',
                    replacement: messages1
                },
                {
                    match: 'messages2',
                    replacement: messages2
                },
                {
                    match: 'messages3',
                    replacement: messages3
                }
            ]
        }))
        .pipe(concat('rules-compiled.json'))
        .pipe(jsonminify())
        .pipe(gulp.dest('./rules/'));

});

The code above finds specific .json files in the rules directory and puts the contents into variables. Then, the matching strings in the rules-template.json file are replaced with the contents of the variables. That output is minified (thanks @mhartington & @jremsikjr !). Finally, the rules are written to 'rules-compiled.json'.

Output:

{"rules":{"room_names":{".read":true,"$room_id":{".validate":"newData.isString()"}},"messages1":{"$room_id":{".read":true,".validate":"root.child('room_names/'+$room_id).exists()","$message_id":{".write":"!data.exists() && newData.exists()",".validate":"newData.hasChildren(['name', 'message', 'timestamp'])","name":{".validate":"newData.isString() && newData.val().length > 0 && newData.val().length < 20 && !newData.val().contains('admin')"},"message":{".validate":"newData.isString() && newData.val().length > 0 && newData.val().length < 50"},"timestamp":{".validate":"newData.val() <= now"},"$other":{".validate":false}}}},"messages2":{"$room_id":{".read":true,".validate":"root.child('room_names/'+$room_id).exists()","$message_id":{".write":"!data.exists() && newData.exists()",".validate":"newData.hasChildren(['name', 'message', 'timestamp'])","name":{".validate":"newData.isString() && newData.val().length > 0 && newData.val().length < 20 && !newData.val().contains('admin')"},"message":{".validate":"newData.isString() && newData.val().length > 0 && newData.val().length < 50"},"timestamp":{".validate":"newData.val() <= now"},"$other":{".validate":false}}}},"messages3":{"$room_id":{".read":true,".validate":"root.child('room_names/'+$room_id).exists()","$message_id":{".write":"!data.exists() && newData.exists()",".validate":"newData.hasChildren(['name', 'message', 'timestamp'])","name":{".validate":"newData.isString() && newData.val().length > 0 && newData.val().length < 20 && !newData.val().contains('admin')"},"message":{".validate":"newData.isString() && newData.val().length > 0 && newData.val().length < 50"},"timestamp":{".validate":"newData.val() <= now"},"$other":{".validate":false}}}}}}

Goals Accomplished

Now, I can easily read each node's rules file without my eyes crossing. Once I paste the minified rules into the dashboard, I'm done. I also feel more confident that someone else (or me in a cowboy coder, gotta fix the production issue right now, mode) will see that minified JSON and know not to touch it.

What do you think? Have suggestions to improve this? Let me know on Twitter @calendee.