Define the domain
The application provides the QLiveDomain bean the rest of QLive reads its
schema from. What goes into it is described in
Defining the GraphQL Domain;
this page is how to build it.
We will follow the example code in qlive-test here. The QLiveDomain bean is created in QLiveDomainConfiguration
Create the bean
Section titled “Create the bean”The bean creation is split into two methods so that it can be reused for tests.
@Configurationpublic class QLiveDomainConfiguration {
@Bean public QLiveDomain qLiveDomain() throws IOException { return newDomain( dslContext, applicationContext.getBeansWithAnnotation(QLiveLogic.class).values(), applicationContext.getBeansOfType(MetadataProvider.class).values() ); }
static QLiveDomain newDomain( DSLContext dslContext, Collection<Object> logicBeans, Collection<MetadataProvider> metadataProviders ) throws IOException {
return QLiveDefaultDomain.newDomain(dslContext, metadataProviders)
// ...
.build(); }}qLiveDomain is the bean creation method that looks up all beans annotated with @QLiveLogic and all beans implementing
MetadataProvider.
newDomain is the static creation method that gets called from qLiveDomain and it has package access to be accessed
by tests within the same package.
QLiveDefaultDomain.newDomain(dslContext, metadataProviders) creates the domain builder that can be configured for the
application and then be built by calling the final .build(). It standardizes the QLive scalars (QueryConfig,
Condition, FieldExpression, ComputedValue and the rest) into the domain.
QLiveDomain is the whole of what the framework and your own code read the
domain through: getGraphQLSchema(), getTypeRegistry() and
getMetaData(). Declare the bean as that rather than as the QLiveDomainImpl
the builder returns – the schema assembly behind it is not something an
application has anything to say to.
Include the jOOQ schema
Section titled “Include the jOOQ schema”To include the POJOs generated by jOOQ in our domain, we just have to import the database schema we want, most often
Public.PUBLIC.
import io.github.qlivedev.qlivetest.domain.Public;
@Configurationpublic class QLiveDomainConfiguration {
static QLiveDomain newDomain( DSLContext dslContext, Collection<Object> logicBeans, Collection<MetadataProvider> metadataProviders ) throws IOException {
return QLiveDefaultDomain.newDomain(dslContext, metadataProviders)
.objectTypes(Public.PUBLIC)
// ...
.build(); }}Of course, the class name of your Public will be different. It is created in the root of where your jOOQ codegen configuration puts all classes.
Include the logic beans
Section titled “Include the logic beans”@Configurationpublic class QLiveDomainConfiguration {
static QLiveDomain newDomain( DSLContext dslContext, Collection<Object> logicBeans, Collection<MetadataProvider> metadataProviders ) throws IOException {
return QLiveDefaultDomain.newDomain(dslContext, metadataProviders)
.logicBeans(logicBeans)
// ...
.build(); }}Define relations
Section titled “Define relations”A relation connects two domain types. It defines what GraphQL fields result of the relation on each side of the relation and it can define the name of those fields.
import static io.github.qlivedev.qlivetest.domain.Tables.FOO;
@Configurationpublic class QLiveDomainConfiguration {
static QLiveDomain newDomain( DSLContext dslContext, Collection<Object> logicBeans, Collection<MetadataProvider> metadataProviders ) throws IOException {
return QLiveDefaultDomain.newDomain(dslContext, metadataProviders)
.configureRelation(FOO.OWNER_ID, SourceField.OBJECT_AND_SCALAR, TargetField.MANY)
// ...
.build(); }}Here we import the jOOQ tables we are going to use as static imports.
The relation definition is based on the a field inside the TableImpls generated by jOOQ.
The SourceField enum defines what fields to create for the source side / the side the foreign key field is defined.
- NONE - No field
- SCALAR - Just the scalar field
- OBJECT - Just the object field as embedded type
- OBJECT_AND_SCALAR - Both the scalar field and the embedded type field.
TargetField defines what field to create on the target side / the side the foreign key points to.
- NONE - No field
- ONE - An embedded object. The relation is assumed to be a to-one relation
- MANY - An embedded array. The relation is assumed to be a to-many relation
Here Foo.ownerId creates a field for both Foo.ownerId and Foo.owner. The relation points to AppUser who receives
a foos array containing all foos that belong to that user.
If you don’t define an embedded object, it will receive its name from the key field name minus “Id”. If you don’t define the name for an embedded array, QLive will use a pluralization function to create the plural of your relation field name without “Id”. By default QLive uses evo-inflector, an abstract rule-based English pluralization function.
Many-to-many
Section titled “Many-to-many”A many-to-many relation lives in a link table with a foreign key to each side. In qlive-test, bar_link associates
bars with bazes:
"Generated from public.bar_link"type BarLink { "DB column 'bar_id'" barId: String! "DB column 'baz_id'" bazId: String! "DB column 'id'" id: String! "DB column 'version'" version: String}Declare it with configureManyToMany(), naming the two foreign key fields and the field each side gets:
import static io.github.qlivedev.qlivetest.domain.Tables.BAR_LINK;
@Configurationpublic class QLiveDomainConfiguration {
static QLiveDomain newDomain( DSLContext dslContext, Collection<Object> logicBeans, Collection<MetadataProvider> metadataProviders ) throws IOException {
return QLiveDefaultDomain.newDomain(dslContext, metadataProviders)
.configureManyToMany(BAR_LINK.BAR_ID, BAR_LINK.BAZ_ID, "bazes", "bars") // ...
.build(); }}Bar gets a field bazes listing its bazes, and Baz a field bars listing its bars. Both are fetched through
bar_link:
type Bar { "Many-to-many objects through bar_link.bar_id" bazes: [Baz]! # ...}Pass null for a side that should get no field.
Each foreign key has to point to the id of its side. Beyond the two foreign keys, the link table needs nothing: an
id and a version column are fine, and QLive fills both in when it writes a link, but neither is required. A
composite primary key over (bar_id, baz_id) works as well. Do give the link table a unique key over its two foreign
keys. Without one, QLive logs a warning at startup, because two users adding the same association at the same moment
could create it twice.
How to change which bazes a bar has is in Edit rows with a working set.
Link tables with columns of their own
Section titled “Link tables with columns of their own”If the link table has a required column of its own – NOT NULL without a default, a role say – the two fields are
read-only, since QLive cannot insert a link row from the two foreign keys alone. Such a link is an entity of its own.
Configure its foreign keys as ordinary relations with configureRelation() and edit the link rows as rows. A table can
have both: the many-to-many declaration leaves relations over the same foreign keys alone.
Without foreign keys
Section titled “Without foreign keys”A link table without foreign key constraints is declared by naming the fields of its type, the same way as a relation without a foreign key:
.withManyToMany( new ManyToManyBuilder() .withPojoFields(BarLink.class, "barId", Bar.class, "bazId", Baz.class) .withFieldNames("bazes", "bars") )Without foreign key
Section titled “Without foreign key”Without a foreign key backing the relation, you need to use the alternate, longer configuration:
.withRelation( new RelationBuilder() .withPojoFields( Waldo.class, Collections.singletonList("userId"), AppUser.class, Collections.singletonList("id") )
.withSourceField(SourceField.OBJECT_AND_SCALAR) .withLeftSideObjectName("user")
.withTargetField(TargetField.MANY) .withRightSideObjectName("waldos") ).withPojoFields(), defines the source type, the virtual foreign key field(s), the target type and the field(s) the
virtual foreign key is pointing to. The rest configures the fields as the shorter method, only with individual “withers”.
Metadata providers
Section titled “Metadata providers”Every MetadataProvider bean the context holds is handed to the domain by
qLiveDomain. Each one writes into
the DomainMeta the server embeds in the page, on three levels: an
addendum next to types, genericTypes, relations and manyToMany, type metadata, and field metadata on individual
fields.
The client-side counterpart is declaration merging – name your addenda
once and they are typed everywhere the application reads config().meta:
declare module "@qlivedev/qlive-ts" { interface DomainMeta { quickSearchTypes: string[] } interface DomainFieldMeta { quickSearch?: boolean }}Nothing but a test on the Java side notices when the two drift apart, so it is worth having one.
The query config a type suggests
Section titled “The query config a type suggests”QueryConfigMetadataProvider is the one QLive brings. It writes the page
size, the sort and the standing condition a row type gets when nothing
names one:
@Beanpublic MetadataProvider queryConfigMetadata(){ return QueryConfigMetadataProvider.newProvider() .forAllTypes() .pageSize(20) .andForType(Foo.class) .sortFields("name") .maxPageSize(100) .build();}forAllTypes() is the house rule for every row type the domain has a query
document for; forType(Class) and forTypes(Class...) are the departures
from it. Chain statements with andForType(), andForTypes() and
andForAllTypes(), and close the chain with build(). A type may only be
named once – say it with forAllTypes() and depart from it per type
rather than declaring the same type twice.
maxPageSize
Section titled “maxPageSize”A page size of 0 asks for every row there is, and a config comes from a
browser. maxPageSize is the ceiling.
Every query over rows of that type is held to it, whoever asked and however
the config got there – an injection, an update(), a logic bean building
one by hand. A larger page, and an unlimited one, become that page size. A
type that declares none is unlimited, which is what every type is until an
application says otherwise.
The config that comes back on the document says the page size that was
applied, so a client that hit the maximum sees it: rowCount is still
everything the condition matches, and a cut page does not look like the
last page of a short table. The maximum itself travels to the client as
type meta data under maxPageSize, for the page size controls that would
rather not offer what the server will not give.
Build the domain in a test
Section titled “Build the domain in a test”Nothing in newDomain touches the DSLContext until a query executes, so a
test can build the same schema with null and assert on it without a
database. Hand it the same logic beans and metadata providers, or you are
testing a different schema than you ship.