Symbol picture: Injection needle with GraphQL-Logo

In this blog post I will try to explain how we organize data loading in Automaton/DomainQL and walk you through the general idea of it and how to adapt it for your own project.

Conceptually it provides an alternative to asynchronous loading / suspense on application startup.

Background

We are working on the relaunch of a German state-level government application. We have been doing paid Open-Source development for clients for quite some time now and are now doing it again for this project as requested by our client.

On the most general level, we use DomainQL to drive our GraphQL.

Symbolic DomainQL Explanation: Database + JOOQ + DomainQL is combined to GraphQL

The current workflow is that we start with a PostgreSQL database from which JOOQ generates Java classes, so called POJOs (Plain Old Java Objects, basically normal/anemic Java classes with getters and setters according to the old Java Bean standard).

Basic injection in DomainQL

DomainQL already contains a comparatively simple GraphQL injection method based on exports with magic names. At point I’m close to considering it deprecated since the new system works so much better now.

At first I expected DomainQL to be our main thing, but along the way it became clear to me that we needed more and we added Automaton as an additional layer. Where DomainQL is still very general, Automaton has a clear and distinct vision and a very opinionated way of getting there.

GraphQL injection in Automaton

Let’s start with the basic idea of our kind of data injection. In a traditional Javascript application, the user loads the initial HTML document which contains very little, it downloads all the Javascript bundles and CSS bundles and other resources and then the JavaScript is executed and let’s say React starts rendering an application.

For which it might need a lot of data which it loads asynchronously which necessitates loading indicators, which prompted solutions like Suspense etc.

Often, this is preferable to having the user wait longer but it introduces a lot of additional latency, even if we can manage to tame the visual artifacts with Suspense.

But wouldn’t it be awesome if the components didn’t have to request the data they need because it is already there? What if the server could provide all the data the components need by knowing what they will request before they do?

Static Code Analysis

When I started wondering about how to do this, I was already using the solution, a small, maybe odd library I had written earlier called babel-plugin-track-usage.

Code is just data and as we do it, we never actually use JavaScript as-is, but always transpile it, compress it etc.

babel-plugin-track-usage uses the Babel AST to detect and track statically analyzable calls within your code base.

At first we used it for i18n.

[ "track-usage", { "sourceRoot" : "src/main/js/", "trackedFunctions": { "i18n": { "module": "./service/i18n", "fn": "", "varArgs": true } }, "debug": false } ]

Here we see the plugin configuration for an i18n service. We define that all our sources are under “src/main/js” (we’re using a Maven project layout) and then we define that we want to track the default export of a module “./service/i18n” which accepts variable arguments.

This lets us write expressions like “i18n("Hello {0}", user)” in our code. The function just looks up the first argument in a translation map that contains all the necessary translations because the babel-plugin-track-usage provided the server with all the invocations of our i18n function as long as all arguments are statically analyzable. With the varArgs setting it only considers the first argument and allows non-static analyzable varargs to follow.

In combination with a mini-Webpack-Plugin, babel-plugin-track-usage then exports JSON data for the server to process.

{ "usages": { "./apps/shipping/processes/crud-test/composites/CRUDList": { "requires": [ "@quinscape/automaton-js", "mobx-react-lite" ], "calls": { "i18n": [ ["Foo List"], ["Action"], ["owner"] ] } }, ... } }

“usages” contains a map of relative module names as keys. The “calls” array contains a map of symbolic call names (Here our “i18n”) with a nested array containing an array per function invocation found.

“requires” contains an array with all modules required by that module. This can be used to follow all import dependencies and find all calls starting with a module as starting point.

After I extended the library a few times to support template literals, ES6 imports and also identifiers, it was ready to drive our data injection too.

Direct GraphQL Query injection

Schematic diagram illustrating the injection process explained below

Our apps in automaton correspond to Webpack entrypoints. Each of them contains a number of processes that make up the application functionality.

For each process there is a MobX class that defines the process scope containing all the data for the process.

Here is how our first version of GraphQL injection looked and still does, although we rarely use it like this

import { observable } from "mobx"; import { query } from "@quinscape/automaton-js" export default class ExampleScope { @observable injected = query( // language=GraphQL `query iQueryFoo($config: QueryConfigInput!) { iQueryFoo(config: $config) { # ... } }`, { "config": { "pageSize": 5, "sortFields": ["name"] } } ) }

We important the “query” function from our library which is registered tracked. The function takes a simple template literal string with the query and a static JSON block with the default config as second argument.

The naming conventions for our file tell the server which process the statically analyzable query belongs to. We follow all imports etc.

When the user starts the application with one of its processes, the server can look up the query() calls it found and use that to fetch the data from GraphQL service.

This method works very well, even when we inject many queries. We don’t even need to bother to craft GraphQL documents with multiple queries. All injected queries will be executed together using an internal shortcut without using HTTP. (Hurray for GraphQL’s transport neutrality).

At the end we can then embed all the queried data in our initial HTML object as script tag with fantasy type.

Our system then can provide the process with a new instance of the process scope that magically contains all the injected data blocks and the React components involved can start rendering right away.

The main reason we switched to a different method is that we in fact have a lot of queries to execute in some processes which makes the process scope very lengthy

Indirect GraphQL injection

At first I couldn’t quite figure out an elegant solution until I realized that like so often, the solution to the problem is another layer of indirection.

What we did was to reify the query arguments into its own named thing and add a second tracked function.

We split the injection into multiple files.

First we have the query definition which we put into its own file.

./src/queries/Q_Foo.js
import { query } from "@quinscape/automaton-js" export default query( // language=GraphQL `query iQueryFoo($config: QueryConfigInput!) { iQueryFoo(config: $config) { # ... } }`, { "config": { "pageSize": 5, "sortFields": ["name"] } } )

We have a standard folder where all our named query definitions are located (here shortened to ./src/queries/). Each definition is named after the file it is in, here Q_Foo.

Now to inject the current value of Q_Foo we just need to write

./src/processes/example/example.js
import { observable } from "mobx"; import { query, injection } from "@quinscape/automaton-js" import Q_Foo from "../../queries/Q_Foo" export default class ExampleScope { @observable injected = injection(Q_Foo) }

I just love the way it works out. The Javascript syntax is just like one would normally write it, I can use my IDE to ctrl+click on the the name and lookup the definitions (or do quick lookups etc). Everything is neatly organized and the scope definitions are much clearer.

The plugin just generates a special JSON block for our identifier

{ "usages": { "./src/processes/example/example": { "requires": [ "mobx", "@quinscape/automaton-js", "./src/queries/Q_Foo" ], "calls": { "injection": [ [ { "__identifier": "Q_Foo" } ] ] } } } }

The JSON data just provides the name and the server has to know / define what that means, in our case “Hey there should be a ‘./src/queries/Q_Foo’ which in turn contains a query() invocation that defines what data we want here.”

Links