How do you write FQL in JavaScript?

I started with the expected q.Match(q.Index("Some_index")) which is not very pleasant to read.

Then I saw how @databrecht does it in his Twitter clone app which does produce a much nicer result. He makes references to the FQL functions he is going to need for the current file and then just writes clean FQL:

const {Match, Index} = q;
const query = Match(Index("Some_index"));

This looks much better, but the pain here is that we need to manually make references to each new FQL function we want to use in the file.

A kinda ugly solution is to just make references to all FQL commands and be done with it :joy:

Now in all seriousness, I’ve been thinking about just writing an FQL-to-JS parser to just write the queries in .fql files. Then either parse via a Webpack loader, or parse the files AOT. The idea would be to write something like this:

Match(Index("Some_index"), $something$)

And get back something like:

const faunadb = require('faunadb');
const q = faunadb.query;

function query (something) {
  return q.Match(q.Index("Some_index"), something);
}

module.exports = query;

It seems like it should be easy to get a naive POC going on.

So how do you write FQL in JavaScript?

2 Likes

Most of the time, I do just the simple const { query: q } = require('faunadb') and use q throughout.

What I do after that has been dependent on the environment.

Helper functions

pretty much no matter what, I am leaning on helper functions in JS that help compose more complex queries. Lots of helpers like UpdateOrCreateDoc, UpdateOrCreateFunction, ExistsOrAbort, PaginateWithOptions.

A lib like faunadb-fql-lib fills in some of the blanks with FQL, but what I mean is wrapping more significant things into JS functions.

Bootstrap scripts

Most of the FQL I right is built in to start up scripts. Step 1, upload GraphQL Schema if desired; Step 2, run a great GREAT many bootstrap queries. My opinion is that any non-trivial queries should be UDFs.

What I have been trying to do is find a most convenient way of maintaining all of the bootstrap code, breaking down into smaller and readable chunks, separating the FQL from the error handling/debuggins (console logging).

I’ve taken to making a lists of “actions” exported from different files.

actions: [
 { 
    description: 'Function: CreateThingForUser`,
    query: 'createOrUpdateFunction({... functionParams})
  },
  ...
]

From there, can import them from many different files, map over and execute them.

Lambda vs. arrow function

The JS driver does not accept an arrow function with 0 arguments as a replacement for Lambda. This makes it not possible to be consistent with using it everywhere.

Because of this I avoid using arrow functions directly in FQL. I will create helpers and use those directly, but you shouldn’t see them directly inside Map, CreateFunction, etc.

// NOT okay
q.Map(set, (ref) => q.Get(ref)))
// Okay
q.Map(set, q.Lambda(['ref'], q.Get(q.Var('ref')))
// -or-
const getRef = (ref) => q.Get(ref)
q.Map(set, getRef)

Client queries and serverless functions

I’ve not tried to be fancy here in production code yet, though I’ve experimented with a few things. Like I said, I’ve stuck with using q.FQL_FUNCTION everywhere.

Now that I’ve uploaded pretty much everything to UDFs, though, all queries are pretty small:

const result = await client.query(
  q.Call(q.Function('AllMyThings'), 64, null, null)
)

Only longer if I need to map, but only so much due to helpers

// helper
const getRef = (ref) => q.Get(ref)

// query
const result = await client.query(
  q.Map(q.Call(q.Function('AllMyThings'), 64, null, null), getRef)
)

Fancy things I’ve tried

Lens library

Forget looking at any FQL! Replace all common view and update operations with lenses, a la shades

const result = await client.query(
  get(documents(), all(), getRef(), 'data', 'name')(q.Collection('User'))
)

(Ok, still have to use some FQL directly)

Having a “lens” that can work on both a query and the returned data was neat to work with.

This was fun, but I have not been so impressed that I am too drawn to keep working on it. I’ve concluded that when I want FQL, it just ends up so much more complicated than that!

Hardcore functional

Creating a library that works with fp-ts and io-ts along with Fauna has led to some interesting coding options, including the following contrived snippet.

A decoder can ensure that the query result matches the expected type, and you can feel confident about working with the results.

pipe(
  TE.taskEither.of<AppErrors, NodeJS.ProcessEnv>(process.env),
  TE.chain(validateEnv),
  TE.chain(flow(createClient, createNewUser))
)().then(
  flow(
    E.chain(decodeUserDocument),
    E.fold(
      (e) => console.error(e),
      (user) => {
        console.log('id', user.ref.id)
        console.log('id', user.ref.collection.id)
        console.log('id', user.data)
      }
    )
  )
)
4 Likes

Thanks for making this Pier, this is useful stuff for people new to FQL!