How to increment a counter stored in database using Faunadb and GraphQL?

Is there a way to increment a counter stored in the database using Faunadb and GraphQL? I am looking to make something similar to dev.to react buttons but what I am worried about is that if I fetch the react counts and then update them on the client-side this will create problems as many people might be updating the react counts at the same time.

I think the answer might be something around user-defined functions and inbuilt functions like add() in FaunaDB but I can’t quite figure it out.

Hi @toughyear and welcome,

let’s suppose you have a simple counter document like this:

 data: { counter: 0 } }

you can create an UDF and execute the increment server-side:

CreateFunction({
  name: "incrementCounter",
  role: null,
  body: Query(
    Lambda([],
      Let(
        {
          counter: Get(Ref(Collection("counter"), "276990384537600517")),
          counterRef: Select(['ref'],Var('counter')),
          counterValue: Select(['data','counter'],Var('counter'))
        },
        Update(Var('counterRef'),{data:{counter:Add(Var('counterValue'),1)}})
      )
    )
  )
}
)

Hope this helps.

Luigi

1 Like

Thanks for the prompt answer, Luigi. Would it be possible for you to explain the Lambda function briefly (what it is doing here) and an example of how to call this on the client-side (like a POST request with mutation query and using @resolve ??)

I know this might be a bit too much. Let me know if you can help with this.

Hi @toughyear,

the function can be a bit more readable this way:

CreateFunction({
  name: "incrementCounter",
  role: null,
  body: Query(
    Lambda([],
      Let(
        {
          counterRef: Ref(Collection("counter"), "276990384537600517"),
          counter: Get(Var('counterRef')),
          counterValue: Select(['data','counter'],Var('counter'))
        },
        Update(Var('counterRef'),{data:{counter:Add(Var('counterValue'),1)}})
      )
    )
  )
}
)

What the Lamda() does is pretty simple, it’s executed without parameters and internally, Let() assigns 3 variables,

  • counterRef: the counter Ref itself
  • counter: the counter document (retrived by Get())
  • counterValue: is the actual counter value
    the second part of Let() is the document update, by adding 1 to the current value.

On how to call that function using @resolver you might take a look at the post Brecht wrote few weeks ago. It’s really well explained with lot of explanation.
You can find the post here.

Let me know in case you need any further help.

Luigi

1 Like

If 1000s of users in your application are writing to the same document at the same time keep in mind that this might create conflicts which could slow down writes.

This is something you shouldn’t worry about prematurely except in case you are already sure that this is going to happen. If that happens there are techniques to work around that, in essence you would split up the count in multiple counts. For example, if the data model allows it you could consider splitting up the counts in multiple documents per user, per group of users, etc. If you don’t have something in your model where it makes sense to split counts into you could just instead of creating one document, create N count documents (e.g. 10 documents) to keep one specific count.

On write, you would randomly pick one of these documents (or the document linked to the user or user group if you took that approach).
On read, you sum the write. So it’s a tradeoff between a less performant read but a more performant write.

As I said, only worry about that when your going to heavily write with many users to one document but I thought it was important to know :slight_smile:

1 Like

It makes more sense now. Is it possible to merge the two operations of Ref and Get, i.e. can we do something like this :

 counter: Get(Ref(Collection("counter"), "276990384537600517")),

You can, but you have to extract the Ref for the update operation then, not sure it simplify things here…
You can use a third version by passing the Ref() as function parameter:

CreateFunction({
  name: "incrementCounter",
  role: null,
  body: Query(
    Lambda(['counterRef'],
      Let(
        {
          counter: Get(Var('counterRef')),
          counterValue: Select(['data','counter'],Var('counter'))
        },
        Update(Var('counterRef'),{data:{counter:Add(Var('counterValue'),1)}})
      )
    )
  )
}
)

and call that way:

Call(Function('incrementCounter'),Ref(Collection("counter"), "276990384537600517"))

Hope this helps.

Luigi

1 Like

So I got the function working like this -

Query(
  Lambda(
    ["articleID", "rxnType"],
    Let(
      {
        articleDoc: Get(Ref(Collection("Article"), Var("articleID"))),
        docRef: Select(["ref"], Var("articleDoc")),
        rxnValue: Select(["data", Var("rxnType")], Var("articleDoc"))
      },
      Update(Var("docRef"), { data: { heart: Add(Var("rxnValue"), 1) } })
    )
  )
)

Is it possible to have dynamic keys in the Update function? What I mean by that is can the update function be something like this -

  Update(Var("docRef"), { data: { Var("rxnValue"): Add(Var("rxnValue"), 1) } })

Basically, can I Var(“rxnValue”) as a Key for the JSON in Update function?

Hi @toughyear,

something like that should work:

Update(Var("docRef"), { data: ToObject([[Var('rxnValue'),Add(Var('rxnValue'),1)]]) })

give it a trial and let me know.

Luigi

Error: [
 {
   "position": [],
   "code": "call error",
   "description": "Calling the function resulted in an error.",
   "cause": [
     {
       "position": [
         "expr",
         "in",
         "params",
         "object",
         "data",
         "to_object",
         0
       ],
       "code": "invalid argument",
       "description": "Field/Value expected, Array provided."
     }
   ]
 }
]

There was a small typo I guess -

data: ToObject([[Var("rxnType"), Add(Var("rxnValue"), 1)]])

It was rxnType in the first Var.

It’s working though.