Tag: Open Source

InteractiveQuery in Automaton

For the second post in our “Cool shit in Automaton” series, I’d like to write about the InteractiveQuery mechanism. From the general conception to the final implementation with some nice Automaton features.

This post might be conceptually interesting for everyone involved with GraphQL, concrete it is most useful if you’re using Automaton.

Design

The InteractiveQuery mechanism was designed to drive the data needs of our widgets. It is a set of high-level abstract query definition types that offer interactive pagination, filtering, sorting and configuring of GraphQL queries.

It touches many Automaton subsystems which I will discuss in some detail here although it is not strictly necessary to understand all of it to use it. In many ways it’s pretty magical.

Defining an InteractiveQuery based GraphQL Query

One of design goals was of course user-friendliness but not at the expense of power and general applicability. A part of that being that we want to write the least amount of code possible, but still retain all the flexibility we need in our client’s domains.

InteractiveQuery turned out to be the driving force behind the Degenerification feature in DomainQL. The basic idea is that we want to be able to use Generics in Java and translate that into the GraphQL type world.

Let’s look at the default GraphQL query implementation for an example entity.

    /**
     * Default implementation of an InteractiveQuery based query for type [T].
     *
     * @param type
     * @param env
     * @param config    configuration for the Interactive query.
     * @param <T>
     * @return
     */
    @GraphQLQuery
    public <T> InteractiveQuery<T> iQuery(

        @GraphQLTypeParam(
            types = {
                Foo.class
            }
        )
        Class<T> type,
        DataFetchingEnvironment env,
        @NotNull QueryConfig config
    )
    {

        log.info("iQuery<{}>, config = {}", type, config);

        return interactiveQueryService.buildInteractiveQuery( type, env, config)
            .execute();

    }
Code language: Java (java)

This is the standard implementation for InteractiveQuery based GraphQL queries. The first method parameter “type” controls the generation of types for this generic method. For each concrete type listed, a degenerified variant of the iQuery method will be defined, by default appending the name of the concrete type to the method method name (here “iQueryFoo”). This is not the most pretty solution but it does keep things in order especially with huge domains. Our app schema already contains 42 variants of this and we’ve just started.

InteractiveQuery in GraphQL

Let’s take a look at the GraphQL types generated from that Java code snippet.

type QueryType {
    "Default implementation of an InteractiveQuery based query for type Foo."
    iQueryFoo(config: QueryConfigInput!): InteractiveQueryFoo
}

"Interactive Query with Foo payload."
type InteractiveQueryFoo {
    "Column states for the current result."
    columnStates: [ColumnState]
    "Query configuration the current result was produced with."
    queryConfig: QueryConfig
    "Total row count available."
    rowCount: Int
    "List with current rows of Foo."
    rows: [Foo]
    "Name of payload type (always 'Foo')"
    type: String
}

"The state of a column within an interactive query."
type ColumnState {
    "True if column is enabled. Server might disabled columns."
    enabled: Boolean
    "Column name"
    name: String
    "True if the column is sortable."
    sortable: Boolean
}

"Encapsulates all parameters of an interactive query."
type QueryConfig {
    "FilterDSL condition graph or null"
    condition: Condition
    "Current page within the paginated results"
    offset: Int
    "Optional unique query identifier. Useful for server-side query implementations."
    id: String
    "Maximum number of paginated results.,"
    pageSize: Int
    "Current sort order for the query."
    sortFields: [FieldExpression]
}

"Map graph representing JOOQ conditions"
scalar Condition

"Map graph representing a JOOQ field expression"
scalar FieldExpression
Code language: JavaScript (javascript)

The iQueryFoo query returns the InteractiveQueryFoo type which is a degenerification of InteractiveQuery<T> for the type Foo.

The InteractiveQuery types are all structurally the same and only differ in the payload type. The “rows” field contains a List of the payload type.

Filtering and Sorting

Here is where the real magic starts. So we want to filter a query, but we don’t want to write code for it. The widget provides a JSON description of a filter which we then apply in the context of the current SQL query.

FilterDSL

There is another complication in that while we / Automaton clearly prefers PostgreSQL as database, our clients may or may not, so we will support all databases that JOOQ supports.

Which also means that we wanted to avoid mucking around with SQL in that degree in any case, i.e. we needed an actual abstraction.

Luckily, JOOQ comes with it’s own Condition API which we can just adapt.

So we created the FilterDSL which is a pretty close copy of the JOOQ condition DSL with some necessary additions.

./src/main/js/apps/myapp/queries/Q_Foo.js
import { query } from "@quinscape/automaton-js"

export default query(
    // language=GraphQL
        `query iQueryFoo($config: QueryConfigInput!)
    {
        iQueryFoo(config: $config)
        {
            type
            columnStates{
                name
                enabled
                sortable
            }
            queryConfig{
                id
                condition
                currentPage
                pageSize
                sortFields
            }
            rows{
                id
                name
                description
                flag
                type
                owner{
                    id
                    login
                }
            }
            rowCount
        }
    }`,
    {
        "config": {
            "pageSize": 20
        }
    }
)
Code language: JavaScript (javascript)

Here we see the query definition to query our Foo type. It has fields of different scalar types and an embedded owner object with an id and a login name.

Now let’s define a filter for that query

import { FilterDSL } from "@quinscape/automaton-js";
import Q_Foo from "../../queries/Q_Foo";

// deconstruct FilterDSL methods
const { field, value, and, or, not } = FilterDSL;

Q_Foo.execute({
        config: {
            condition : and(
                field("name").eq(value("AAA")),
                not(
                    field(value("owner.login")).eq("admin")
                )
            )
        }
    })
    .then(
        ({iQueryFoo}) => {
            // ...
        }
    )Code language: JavaScript (javascript)

We query all Foo objects whose name is “AAA” and which are not owned by “admin”.

You can choose how you want to write your boolean conditions. Either like above or you could write the same condition as

field("name").eq(
    value("AAA")
).and(
    field(
        value("owner.login")
    ).eq("admin")
    .not()
)Code language: CSS (css)

or a mix in between. Personally I find the second style not so good and the dangling .not() is outright horrible.

Using value()

To define values for our comparisons, we need to wrap the Javascript values in the value() method.

value(val, type = getDefaultType(value))Code language: JavaScript (javascript)

Most of the time you can get away with simply wrapping your JavaScript values with value(). The scalar type is then chosen based on the JavaScript type. If you use special number types or if you need a “Date” you must define that type as second argument.

While the FilterDSL uses prototype chaining to provide the API, it returns plain Javascript objects and arrays.

For our example condition the JSON would be

{
    "type": "Condition",
    "name": "and",
    "operands": [
        {
            "type": "Condition",
            "name": "eq",
            "operands": [
                {
                    "type": "Field",
                    "name": "name"
                },
                {
                    "type": "Value",
                    "value": "AAA",
                    "scalarType" : "String",
                    "name": null
                }
            ]
        },
        {
            "type": "Condition",
            "name": "not",
            "operands": [
                {
                    "type": "Condition",
                    "name": "eq",
                    "operands": [
                        {
                            "type": "Field",
                            "name": "owner.login"
                        },
                        {
                            "type": "Value",
                            "value": "admin",
                            "scalarType" : "String",
                            "name": null
                        }
                    ]
                }
            ]
        }
    ]
}Code language: JSON / JSON with Comments (json)

We use a special Condition scalar to transport these condition graphs through GraphQL without having to select the fields, which we couldn’t anyway except by setting arbitrary complexity limits on our filters.

Sorting

Just like our Condition scalar is a object equivalent to the WHERE clause, we can define the ORDER BY clause by using the sortFields field / the FieldExpression scalar.

The FieldExpression scalar is a close cousin of the condition API with a special shortcut version.

Most commonly we want very simple sorting

sortFields: ["name"]Code language: CSS (css)

Sort by name

sortFields: ["owner.login", "!name"]Code language: CSS (css)

Sort first by owner name and then descending by name.

And because the implementation was nearly trivial, we can also do complex sorting.

sortFields: [ field("numA").add(field("numB")).desc() ]Code language: CSS (css)

Sort descending by the sum of the fields numA and numB.

Generalized Filtering

The FilterDSL started out and was designed to be transformed into a JOOQ condition and finally an SQL WHERE clause, but in the end it is a pretty abstract filter definition language.

Just like we did in OpenSAGA, we’re using this to filter different things in different contexts.

Automaton contains a transformer that transforms FilterDSL condition graphs into a hierarchy of Java filter classes that can be used to filter Java object instances.

So when we implemented Websocket pubsub for Automaton we could use that to implement a nice topic based pubsub that can execute user-defined filters to decide who wants to see what message on each topic.

So when we have an application of that like the useDomainMonitor hook, we can very succinctly tell the hook what we’re interested in.

    const monitor = useDomainMonitor(
        field("domainType")
            .eq(
                value("Foo")
            )
    )Code language: JavaScript (javascript)

Above code will create a monitor instance that receives all domain monitoring meta data for our Foo type. But we could just as well limit that to a single entity or a mix of entity types.

Working with results

In our example above, we receive the iQueryFoo object which is an InteractiveQueryFoo instance.

Now automaton not only does auto-conversion of GraphQL queries to bring the scalar values into the right formats, it also can instantiate MobX classes for GraphQL types and even based on the original type of the degenerified type, which means for InteractiveQueryFoo that we will automatically receive a JavaScript implementation for InteractiveQuery which contains the data fields as observable fields but also offers two methods to continue to work with the query.


// update the iQuery document
iQueryFoo.update(queryConfig)

// Update / merge query conditions (advanced usage)
iQueryFoo.updateCondition(
    componentCondition, 
    componentId = NO_COMPONENT,
    checkConditions = true
)Code language: JavaScript (javascript)

iq.update(config) let’s you update the iQuery document with a new partial query config. For example

iQueryFoo.update({ offset: 20})Code language: CSS (css)

will update iQueryFoo with the rows for the second page. Internally the InteractiveQuery.js implementation will just update the document observable with new values.

This allows for cooperative control of data-sources from e.g. a datagrid widget and an external complex filter form.

Injection Pitfalls

It is possible to provide a default filter for both directly and indirectly injected InteractiveQuery documents with one caveat.

babel-plugin-track-usage is not clever enough to understand our FilterDSL so we can’t use it 🙁

If you find yourself in that situation it’s mostly the easiest to write a little script where you can test a FilterDSL expression and then grab the JSON output of that.

InteractiveQuery Configuration

The columnStates field is used for an advanced feature where a user can customize the GraphQL query. The query definition defines the maximum set of fields and the user can disable fields.

This requires the server-side to store e.g. user-specifc configuration and merge that in query method to use instead of the default full selection.

Complex Query Performance

In spite of being very very complex and versatile, we actually managed to get the standard querying mechanism to perform about as well as possible.

We don’t do cascading fetcher fetching, but we also don’t batch like it is commonly done.

Instead, the InteractiveQueryService looks at the domain / GraphQL types and creates an optimized execution plan that fetches the results in very small number of queries.

For one, if we, like in the example about have our Foo with owner field, we don’t fetch the foo object and then the owner, we actually do a

SELECT "foo"."id", "foo"."name", "foo"."description", "foo"."flag", "foo"."type", "owner"."id", "owner"."login", "foo"."owner_id" 
    FROM "public"."foo" as "foo" 
        LEFT OUTER JOIN "public"."app_user" as "owner" 
        ON "owner"."id" = "foo"."owner_id"
    ORDER BY "foo"."name"Code language: JavaScript (javascript)

that is we join-in foreign key relations right within the initial query.

Only when we follow *-to-many we split off into different queries which are then batched in themselves. If I fetch ten Foo and go into e.g. a bars field containing a List of associated Bar entities, the InteractiveQuery service will fetch all bar instances with one query.

There’s one limitation in that such a *-to-many “join” will always be a left outer join and not a right join or so.

Outlook

The InteractiveQuery mechanism is something that is really lacking in DomainQL if you should happen to need it. It is however also very opinionated and kind of the one query to rule them all.

It would be one of the top candidates to be moved into its own sub-library.

Links

GraphQL-injection with Automaton/DomainQL

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
        }
    ]
Code language: JSON / JSON with Comments (json)

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"]
                ]
            }
        },
        ...
    }
}Code language: JavaScript (javascript)

“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"]
            }
        }
    )
}Code language: JavaScript (javascript)

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"]
        }
    }
)
Code language: JavaScript (javascript)

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)
}Code language: JavaScript (javascript)

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"
                        }
                    ]
                ]
            }
        }
    }
}
Code language: JSON / JSON with Comments (json)

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

© 2024 fforw.de

Theme by Anders NorénUp ↑