How to do range queries in FaunaDB's GraphQL

How to do range queries in GraphQL? It seems it’s not possible to have a GraphQL query on Fauna that does the equivalent of

SELECT * FROM PRODUCTS where PRICE > 50 AND PRICE < 100

it’s not out-of-the-box possible at this point but it might be in the future. Since everything you do is backed by an index to make it efficient we’ll have to think this through.

You can however ‘easily’ implement it yourself using a user defined function via the @resolver tag. In this case you’ll have to write a custom resolver that you implement on UDF. It’s not really complex, but it’s a bit more involved, you do need to know how to start.

Basically:

Step 1: write a query/mutation with @resolver

Use a resolver on your query or mutation (use the @resolver syntax), this will create a User Defined Function for you on the fauna site that will be called which you still have to fill in (implement the body). By default, it will just Abort and tell you to implement it when you call the query.
For example, this is my schema to write login resolvers.

type Mutation {
  register(email: String!, password: String!): Account! @resolver
  login(email: String!, password: String!): String! @resolver
}

which then can be called as if they were regular GraphQL mutations (however, we didn’t tell FaunaDB what to do yet when we call them, we need to implement the UDF.

Step 2: Create an index to support the range query.

Create an index and put PRICE in values and add the ref as well to actually get the data out there. Let’s say we name it products_by_price, select the products collection.

Note: we add ref here to later on use Get on it, if the collection’s attributes or the data you need from it is not going to change a lot you can add all values you need to return here immediately which will be far more efficient.

Step 3: Implement the UDF

Write a Range query in the body of that function. https://docs.fauna.com/fauna/current/api/fql/functions/range, since FQL might be new for you, let’s do that step by step:

// Note that values are inclusive.
Step 3a
First, let’s just get the references of the products that match our range

Paginate(
    Range(Match(q.Index('products_by_price')), 50.000000001, 99.9999999999) 
 )

Step 3b in case we added the reference to the index (dynamic)
We can’t do a lot with references, except Get them. We retrieved a list of references (one page) here so we can now loop over this list as in a regular programming language with Map.

 Map(
    Paginate(
       Range(Match(q.Index('products_by_price')), 0, 20 // values are inclusive. 
    ), 
    Lambda(['proficiency', 'ref'], Get(Var('ref')))
)

Now we return the complete object which is not ok for GraphQL since that’s not plain JSON, it contains FaunaDB specific structures such as Refs, etc. To return exactly what we need, let’s start structuring our query and our return result we use Let (let is the key to structuring many queries)

Map(
    Paginate(
       Range(Match(q.Index('products_by_price')), 0, 20) 
    ), 
    Lambda(['proficiency', 'ref'], 
      Let({ 
        product: Get(Var('ref'))
      }, 
     // Every variable we define in leet can then used for a return
     // value in the second parameter of let. 
     // let's return a Json with just the name
      {
        name: Select(['data', 'name'], Var('product'))
      })
  )
)

Now we have the query we need (all the above can just be pasted in the dashboard shell to test out your FQL query).

Then we’ll go to functions in the dashboard and we’ll notice that there is a function created for us since we used the @resolve tag. That function is not implemented and looks like:

Just remove the Abort, paste in our query and make sure to set the variables that the GraphQL mutation/query expects.

We should now be able to query via GraphQL.

note1 As mentioned, you could also add all values to the index that you need. That has the advantage that the query will be cheaper. In that case the query will look simpler:

Map(
  Paginate(
    Range(Match(q.Index("products_by_price")), 0, 20) // values are inclusive.
  ),
  Lambda(
    ["price", "somevalue1", "somevalue2",.. ],
    {
      price: Var('price'), 
      somevalue1: Var('somevalue1'), 
      somevalue2: Var('somevalue2')
    }
  )
)

But when an attribute is added, your query needs to change since the lambda has to have the same # of values.

note2 Later on you probably need to add parameters for pagination, check the pagination guide for that specifically.

2 Likes