8

I'm creating an application which lets users create items and then allow other users to subscribe to those items. I'm struggling to craft a rule that will prevent users from subscribing more than once to an item.

Here is an example of my data structure (anonymized, hence the "OMITTED" values):

{
    "OMITTED" : {
        "name" : "Second",
        "body" : "this is another",
        "userName" : "Some User",
        "userId" : "OMITTED",
        "created" : 1385602708464,
        "subscribers" : {
            "OMITTED" : {
                "userName" : "Some User",
                "userId" : "OMITTED"
            }
        }
    }
}

Here are my Firebase rules at present:

{
  "rules": {
    ".read": true,
    ".write": "auth != null",
    "items": {
      "$item": {
        ".write": "!data.exists()",
        ".validate": "newData.hasChildren(['name', 'body', 'userId', 'userName']) && newData.child('userId').val() == auth.id",
        "subscribers": {
          "$sub": {
            ".validate": "newData.hasChildren(['userId', 'userName']) && newData.child('userId').val() != data.child('userId').val()"
          }
        }
      }
    }
  }
}

How can I prevent users from subscribing more than once? What is the rule I need to prevent duplicate users within the subscribers list based on userId?

Stephan Muller
  • 27,018
  • 16
  • 85
  • 126
Soviut
  • 88,194
  • 49
  • 192
  • 260
  • 1
    If you want to prevent multiple entries with the same user id, then the records are unique per id. Thus, use the user id as the key for that record, then THERE CAN BE ONLY ONE : ) – Kato Nov 28 '13 at 15:12
  • Why not put that as an actual answer? – Soviut Nov 28 '13 at 18:12
  • 1
    Because time is a precious commodity, my friend, and not always available at the time that I want to spend it. : ) – Kato Nov 29 '13 at 17:50

1 Answers1

5

Since security rules can't iterate a list of records to find the one containing a certain bit of data, the trick here is to store the records by an ID which allows for easy access. There is a great article on denormalization which offers some good insights into this practice.

In this case, if your use case allows, you may simply want to switch your data structure so that records are stored by the user's id, rather than storing the ID as a value in the record, like so:

/users/user_id/items/item_id/subscribers/user_id/

In fact, as you'll see in denormalization, you may even benefit from splitting things out even farther, depending on the exact size of your data and how you'll be reading it later:

/users/user_id
/items/user_id/item_id
/subscribers/item_id/user_id

In either of these formats, you can now prevent duplicates and lock down security rather nicely with something like this:

{
   "users": {
      "$user_id": { ".write": "auth.id === $user_id" }
   },
   "subscribers": {
      "$subscriber_id": { ".write": "auth.id === $subscriber_id" }
   }
}
Soviut
  • 88,194
  • 49
  • 192
  • 260
Kato
  • 40,352
  • 6
  • 119
  • 149
  • Thanks. This is what I assumed all along but wondered if there was some sort of `in` functionality I was missing. – Soviut Nov 29 '13 at 18:47
  • It's a requested feature and I'm sure it will end up in the security rules at some point! – Kato Nov 30 '13 at 02:02
  • It seems so. an `in` is crucial for any sort of compound key rules, unless they're expecting every application to create compound keys as primary keys. Not unreasonable, but certainly not ideal given the number of possible applications for Firebase. – Soviut Nov 30 '13 at 09:02
  • The difficulty with in() ops is that they are terribly slow. So in a real-time, NoSQL environment like Firebase, it's generally preferably to store the data in a manner that allows you to reference data by keys, or to create additional indexes on fields you want to search. It's a bit more work on write ops to make read ops lightning fast and, if in() ops existed, I would still prefer an index for responsiveness. – Kato Nov 30 '13 at 16:57