DEV Community

Richard Beattie
Richard Beattie

Posted on • Edited on • Originally published at r-bt.com

Firestore Data Validation

For prepsheets.com I use Firebase and Typescript. At the start I sort-of took type-safety to the extreme. I use runtypes to validate all the data that comes into the app and have firestore security rules which enforce a schema on the database. This is somewhat annoying, but I think it's end-positive as I can be sure of what data ends up in the database.


The first step is to ensure only valid field names are submitted (to stop users storing lots of arbitrary data)

function incomingData() {
   return request.resource.data;
}

function onlyHasAttrs(attrs){
   return incomingData().keys().hasOnly(attrs);
}

match /users/{userId} {
    allow create: if onlyHasAttrs(['name', 'email', 'picture', 'age']);
}
Enter fullscreen mode Exit fullscreen mode

This uses the hasOnly function on lists. It checks if all keys in incomingData are in attrs


Next I ensure that fields are off the correct type. This is easy for strings and numbers

function hasValidSchema() {
    return (
        onlyHasAttrs(['name', 'email', 'picture', 'age']) &&
        incomingData().name is string &&
        incomingData().name.size() > 0 &&
        incomingData().age is number
    );
}

allow create: if hasValidSchema();
Enter fullscreen mode Exit fullscreen mode

Unfortunately things are more complicated with lists and maps. While you can ensure that a field is off the type list or map you can't validate the members of those objects. Or at least you can't validate an arbitrary number of them. See: https://stackoverflow.com/a/58257828/3949864. Apparently the firebase team is looking into this so you can help get list type safety faster by filing a feature request.

For map attributes I only allow them to be set or updated to being empty. I use Cloud Functions to actually set the data, validating whatever is passed with runtypes.

function hasValidMap() {
    return (
        incomingData().map is map &&
        incomingData().map.size() == 0 ||
        incomingData().map == existingData().map
    );
}
Enter fullscreen mode Exit fullscreen mode

For lists I either use Cloud Functions to set the data or impose a maximum length. For example say we let users list their interests, but they can only list 5. Then in security rules we can use:

function hasValidInterests(interests) {
    return (
        interests is list &&
        (interests.size() < 1 || interests[0] is string) &&
        (interests.size() < 2 || interests[1] is string) &&
        (interests.size() < 3 || interests[2] is string) &&
        (interests.size() < 4 || interests[3] is string) &&
        (interests.size() < 5 || interests[4] is string) &&
        (interests.size() < 6)
    );
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)