JS Driver: FQL through template literals

I wasn’t sure if I should have instead made this as an issue on the JS driver GitHub, but this is a fairly “general” thing anyways - I suspect this applies to multiple languages.

Currently, making queries is done through a fairly well thought out representation of FQL within JavaScript, creating an object composed of q.Foo() functions. However, with the introduction of template literals a while back into JS, its possible to use a tagged template literal to create FQL query strings.

Using a tag function, any FQL template literal strings could be evaluated and transformed into an instance of the already existing FaunaDB.Expr class. Done like this, there shouldn’t be any compatibility issues, even when used with query functions. I suspect there would be a lot of implementation details to be hashed out, though.

Some of the implications of using template literals are pretty great. For me, it’s primarily three things:

  • Template literal queries would be more portable in general, with theoretically complete compatibility with anything written in Fauna Shell.
  • I think queries are objectively more readable and easier to parse when written in pure FQL.
  • Very slick editor support for FQL queries, as a template literal tagged with fql or FQL could be parsed by an editor as an FQL language string, complete with syntax highlighting and autocomplete, if provided.

I figured (at first) that it wouldn’t be difficult to just create the function myself, so I did a bit of snooping around. I wanted to make a implementation that would look like this:

Client.query(
    fql`Select("fun", [you_get_it])`
)

However, as I looked around the JS driver source I realized the JS functions were not composing an FQL query string as I had originally suspected. So, I instead took a look at the Fauna Shell parser code, and saw that it had to do a lot of work to parse input FQL and create a query.

TLDR, I realized it wasn’t worth doing this myself. Even if I did, I’d have to bundle large portions of the parser code and I didn’t feel like doing that.

So consider this my humble request for this feature. I’m sure a concern is compatibility - but I think feature detection could allow this to exist as an optional supplement to the current query functions. Front-end developers already know to not use template literals if they’re targeting older browsers.

Hi, welcome @Monkatraz!

The query that is sent is not a string, but an abstract syntax tree (AST) of the query to be run. All that work that the drivers do is to convert the composable FQL functions into the AST.

Fauna folks have stated that this is used, rather than strings, to prevent issues with injection. Further, it has also been stated that the internal AST representation is open to change while maintaining the same FQL language, or functions.

You should probably stop using the phrase “query string” because to Fauna there is no such thing :slight_smile:

Now, whether it is useful or not to interpret (effectively a limited/safe version of eval) a string to FQL is still a good question in my opinion. It probably has some uses. I have been wondering how to store some queries in a DB and then fetch and call them from a server (DB setup and/or migration queries). But for the most part, the fact that FQL is based on functions is a REALLY cool thing. You can create helper functions to help compose more complex queries. Trying to do that with string concatenation would be cumbersome and more prone to errors. I don’t think it is likely we’ll see the team take this up, because the functional nature of FQL is one of the major selling points!

As an aside, it should be noted that you can bring every FQL function you want into scope, if it is really frustrating you that you have to add “q.” in front of all of them. i.e.

const { query: q } as require('faunadb')
const { Let, Exists, Collection, Var, Update, /* etc... */ } = q 

See this thread for a related discussion:
How do you write FQL in JavaScript?

Walking through it, here is a contrived example that will upsert a collection and create an index in a single query.

FQL

const createOrUpdateFunction = (params) =>
  q.Let(
    {
      exists: q.Exists(q.Collection(params.name)),
      function: q.If(
        q.Var('exists'),
        q.Update(q.Collection(params.name), params),
        q.CreateCollection(params)
      )
    },
    {
      result: q.If(q.Var('exists'), 'updated', 'created'),
      function: q.Var('function')
    }
  )

// later...

const result = await client.query(
q.Let(
    {
      collection: createOrUpdateFunction({
        name: 'User'
      }),
      collectionRef: q.Select('ref', q.Var('collection'))
    },
    q.CreateIndex({
      name: 'User_unique_email',
      source: q.Var('collectionRef'),
      terms: [{ field: ['data', 'email'] }],
      unique: true
    })
  )
)

Template String

In my opinion this is demonstrably more difficult to write, read, and maintain. Even if you did have good IDE support.

const createOrUpdateFunction = (params) =>
  fql`q.Let(
    {
      exists: q.Exists(q.Collection(${params.name})),
      function: q.If(
        q.Var('exists'),
        q.Update(q.Collection(${params.name}), ${params}),
        q.CreateCollection(${params})
      )
    },
    {
      result: q.If(q.Var('exists'), 'updated', 'created'),
      function: q.Var('function')
    }
  )`

// later...
const result = await client.query(
  fql`q.Let(
    {
      collection: ${createOrUpdateFunction({
        name: 'User'
      })},
      collectionRef: q.Select('ref', q.Var('collection'))
    },
    q.CreateIndex({
      name: 'User_unique_email',
      source: q.Var('collectionRef'),
      terms: [{ field: ['data', 'email'] }],
      unique: true
    })
  )`
)

1 Like

Oops, I think there was a bit of miscommunication.

The template literals would allow to remove the q.[function] syntax, and allow you to just use FQL as you see in the Fauna Shell. Injection is certainly an issue - but why are you using template literals if you are concatenating strings? (You would have to do that to get injection) The FQL tag function would only parse the actual ‘string’ parts of the literal. Any inserted strings would be left alone or parsed differently, as tag functions just let you do that.

Although, since I wrote the original post, the query functions grew onto me - the usage of JS functions for FQL Lambdas is kind of automagical. Also, the AST construction means you can store small bits of it in variables, meaning you can save expressions and reuse them. Making my own little collection of ‘helper expression’ functions is pretty neat. I mean you could do that with the template literal idea but it doesn’t add that much either. I still think the portability and editor support is interesting, but not needed.

Thanks for the response!