2

Is there a Firebase rule so that I can make sure that only one record may exist in a certain table, for example.

enter image description here

When ongoing has a record. It's not possible to add a new one.

count() and length() don't exist for firebase so it seems.

Community
  • 1
  • 1
Miguel Stevens
  • 8,631
  • 18
  • 66
  • 125

3 Answers3

2

It is possible to do what you want with a security rule.

Because your limit for the maximum number of children is one, you can take advantage of the fact that keys with no children do not exist.

If you use a rule like this:

{
  "rules": {
    "ongoing": {
      "$key": {
        ".read": "auth != null",
        ".write": "auth != null && ((newData.exists() && (!data.parent().exists() || data.exists())) || !newData.exists())"
      }
    }
  }
}

The write will be allowed:

  • if the child is being added and the ongoing key does not exist; or
  • if the child is being updated; or
  • if the child is being removed.

The rule will prevent the addition of another child if one already exists (as the ongoing key will already exist).

cartant
  • 57,105
  • 17
  • 163
  • 197
  • I don't think this addresses the question. The OP wants one and only one child node under the ongoing node. These rules allow multiple children to be created. To test, I created a node in Firebase */ongoing/child_0: "test"*. Then in my test app I had a simple *ongoingRef.child("child_1").setValue("test")* and it successfully wrote the second node even though there was already one node. To test further, I changed the app to write multiple random nodes to the ongoing node which was also successful. – Jay Nov 26 '17 at 13:04
  • I tested this, too, before I posted it. It worked fine, for me, and prevented the creation of more than one child. I can revisit my tests, tomorrow, as it's possible that I had an error, but at a conceptual level, I think the rule makes sense, so I don't see why it wouldn't work. When using it, beware of cascading permission grants that must not be made before/above the rule. – cartant Nov 26 '17 at 13:16
  • I got it. I copied and pasted your rules into mine where the "ongoing" rules starts. Just above that, I had *".write": "auth != null"*. Once I removed that, its working perfectly. Great answer, I added one line to your answer "rules" just above "ongoing" so others dont make the same mistake I did. – Jay Nov 26 '17 at 14:13
  • Thanks for your time! It's not working for me :( I'm not using auth so this piece of code should be working, right? https://imgur.com/a/CZgk4 – Miguel Stevens Nov 27 '17 at 21:16
  • Oh it's working when I use .validate instead of .write! Great thank you! – Miguel Stevens Nov 27 '17 at 21:19
  • If you aren't using auth, you can just remove those parts. I included them as it's important that they are **not** used above/before the rule - i.e. in rules further up the hierarchy - as granted permissions cascade down. That is, once a permission is granted it cannot be revoked. Good to hear that it's working for you. – cartant Nov 27 '17 at 21:55
1

You can't express this as a validation rule. What you can do is instead use a Cloud Function to express the logic to accept or reject a change based on existing data. Your client code will need to invoke this function perhaps by a write to a different part of the database, or an HTTPS function.

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
  • Can you take a look at my solution? It works but it feels like I may be missing something to make it more a more complete solution for the OP. – Jay Nov 25 '17 at 13:57
  • Actually, because the maximum number of children is one, it is possible to enforce this using a security rule. See my answer. – cartant Nov 25 '17 at 21:29
1

This may not be exactly what you want but it does provide a solution:

Change your structure from

ongoing
   child_0
      id: "some id"

to

ongoing
    child_count: 1
    child_0
       id: "some id"

and then the rules

{
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null",
    "ongoing": {
        ".validate": "root.child('ongoing').child('child_count').val() < 1"
    }
  }
}

When you initially create the ongoing node, create it like this

ongoing
   child_count: 0

and then when a node is added, change child_count to 1. From there any more attempts to add or modify the ongoing node will be denied.

Jay
  • 34,438
  • 18
  • 52
  • 81
  • You would have to do this all atomically with a transaction, otherwise your count could get mangled under concurrent load. Also you would have to trust your clients to increment the count correctly. Client code can be compromised to bypass the check and add whatever data they want without updating the count. Cloud Functions is probably the best way to ensure consistency. – Doug Stevenson Nov 25 '17 at 14:16
  • @DougStevenson Excellent points! Depending on the use case, this is a potential solution. However, as Doug pointed out his solution is probably better long term. – Jay Nov 25 '17 at 14:21
  • Also see Kato's answer from a few years ago: https://stackoverflow.com/questions/22644202/limit-number-of-records-that-can-be-written-to-a-path-reference-other-paths-in – Frank van Puffelen Nov 25 '17 at 16:33