Skip to content

Expose document queries

Each type of query document needs to be served by its own dedicated endpoint. Thanks to our GraphQL engine we don’t have to write them by hand, but we can instead use one generic endpoint that gets multiplied into one query per row type we declare.

We use this general qlive-graphql pattern to serve out query documents.

Here we see the setup for logic bean containing such a generic QueryDocumentService endpoint.

QueryLogic.java
import io.github.qlivedev.qlive.api.annotation.QLiveLogic;
import io.github.qlivedev.qlive.api.annotation.QLiveQuery;
import io.github.qlivedev.qlive.api.annotation.QLiveTypeParam;
import io.github.qlivedev.qlive.model.QueryConfig;
import io.github.qlivedev.qlive.model.QueryDocument;
import io.github.qlivedev.qlive.runtime.query.QueryDocumentService;
import io.github.qlivedev.qlivetest.domain.tables.pojos.Bar;
import io.github.qlivedev.qlivetest.domain.tables.pojos.Foo;
import graphql.schema.DataFetchingEnvironment;
import jakarta.validation.constraints.NotNull;
import org.springframework.context.annotation.Lazy;
/// Example logic
@QLiveLogic
public class QueryLogic
{
private final QueryDocumentService queryDocumentService;
public QueryLogic(
@Lazy QueryDocumentService queryDocumentService
)
{
this.queryDocumentService = queryDocumentService;
}
/// Queries [T] objects based on the given query config
@QLiveQuery
public <T> @NotNull QueryDocument<T> queryDocument(
@QLiveTypeParam(
namePattern = "query*Document",
typeNamePattern = "*Document",
types = {
Foo.class,
Bar.class
}
)
Class<T> type,
DataFetchingEnvironment env,
@NotNull QueryConfig config
)
{
return queryDocumentService.buildQuery(type, env, config)
.selectByFilter(true)
.execute();
}
}

@QLiveTypeParam controls how the generic method is turned into many concrete methods:

  • types lists all rowTypes we want.
  • namePattern declares the name pattern for the GraphQL method.
  • typeNamePattern declares the pattern for the result type name

So here we declare Foo.class and Bar.class as the row types we want. The result schema corresponding to this will be

type QueryType {
"Queries Foo objects based on the given query config"
queryFooDocument(config: QueryConfig!): FooDocument!
"Queries Bar objects based on the given query config"
queryBarDocument(config: QueryConfig!): BarDocument!
# ...
}

The one option worth understanding. It decides how far a QueryConfig posted by a browser may reach.

  • false (the default) is the strict mode. A filter or sort path may only name a field the query actually selects. That is what makes the query document itself the security boundary.
  • true lets a path extend the plan: relations it crosses get joined, and the field it ends at gets selected. More convenient, and a wider surface – a client can then read through relations the query did not select.

qlive-test passes true because it is a test application that only has security examples and no real security.