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?

3 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)
      }
    )
  )
)
3 Likes

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

1 Like

I think this post could use an update since v4 is being replaced with v10, and the JS library is now “fauna” rather than “faunadb”. I’m still transitioning to v10 myself, but as far as I can tell pretty much all queries to fauna are done with fql strings now.

So as an example of what has changed, in v4 you could add a new collection like so:

const collectionName = "User";

await client.query(q.createCollection({ name: collectionName }));

Now in v10 to achieve the same result you’d have to write it like this:

const collectionName = "User";

await client.query(fql`Collection.create({ name: ${collectionName} })`);
1 Like

A lot of the same techniques involving helper functions and other type-safe patterns can be achieved with query composition. The bits of queries will be strings, but they can still be built with composable javascript objects. Here’s some more documentation about query composition in FQL v10.

This is indeed an old one, though. I’ll mark this one as closed.