Startup and configuration
startup()
Section titled “startup()”function
declare function startup(options: StartupOptions): Promise<Root>;Initializes QLive by
Parameters
options |
StartupOptions
Section titled “StartupOptions”interface
interface StartupOptionsQLive startup options.
StartupOptions.views
Section titled “StartupOptions.views”views?: ViewModules;The application’s view modules, as produced by import.meta.glob().
Vite resolves import.meta.glob() at build time, relative to the file it appears in, and only accepts literal patterns – so the call has to happen in the application, which knows where its views live, and the resulting map is handed to QLive here:
await startup({views: import.meta.glob("./app/**\/*.tsx"), ... })Without the eager option the map holds loader functions, so a view module is only fetched once
loadView() actually asks for it.
Optional: an entry point that renders one fixed page – a login page, say – resolves no routes and has nothing to register.
StartupOptions.init
Section titled “StartupOptions.init”init?: (config: QLiveConfig) => Promise<void>;The init function can be used to modify system behavior after the initialization is done but before a view is rendered.
Parameters
config |
Initialized config |
StartupOptions.render
Section titled “StartupOptions.render”render?: FunctionComponent<{ config: QLiveConfig;In case there are no views defined, the user can give a render function to render the first components after
initialization. The component is rendered in strictMode if that is set to true.
StartupOptions.root
Section titled “StartupOptions.root”root?: string | FunctionComponent<any>;Defines the behavior for visiting the root URL (e.g. /app). If it is a string, we internally redirect to the view with that name. Otherwise, it has to be a function component.
StartupOptions.strictMode
Section titled “StartupOptions.strictMode”strictMode?: boolean;Whether to wrap the views in React.StrictMode. Default is true
StartupOptions.fixture
Section titled “StartupOptions.fixture”fixture?: QLiveFixture;Runs the application on the given fixture instead of on the data a server sends, see initFixture(). Takes the place of the bootstrap the page would otherwise read or fetch; everything else is the same.
noSchema()
Section titled “noSchema()”function
declare function noSchema(): void;Declares that this entry point needs no domain schema.
Call it at the top level of an entry module. The call does nothing at runtime and exists to be seen by the build’s track-usage analysis, which the server reads: a path whose module declares this gets the reduced bootstrap – context path, CSRF token and injections, with an empty schema and empty meta data in place of the domain. On a large domain that is the difference between shipping the whole introspection result and shipping none of it.
Only for entry points that issue no queries and render no application view: a login page, an error page, a public landing page. Anything that resolves a route or reads an injection with a query needs the schema and will fail on its first type lookup without it.
initFixture()
Section titled “initFixture()”function
declare function initFixture(fixture: QLiveFixture): Promise<QLiveConfig>;Initializes QLive from the given fixture, the way init() does from the bootstrap a server sends, and runs it on that fixture from then on.
A view running on a fixture runs unchanged. Its injections come out of the fixture, and its query documents answer update() in the browser: they filter, sort and page the rows the fixture holds, as localDocument() does. Nothing else goes anywhere – graphql() rejects, so nothing is written, and subscribeToTopic() subscribes to nothing, so nothing is pushed.
This is for whatever renders a view without startup(): a Storybook decorator, a component test, an island on a static page. startup({fixture}) runs a whole application on one.
initFixture(fixture)createRoot(element).render(<FooList/>)The config and the injections are module state: initializing another fixture replaces the injections of the last. To run several views on fixtures on one page, add each with addFixture(), or render each in a FixtureScope.
Parameters
fixture |
the fixture, e.g. one recorded from a running application in dev mode |
Returns the initialized config
addFixture()
Section titled “addFixture()”function
declare function addFixture(fixture: QLiveFixture): Promise<QLiveConfig>;Adds the given fixture to those the page runs on, for several views on fixtures on one page: a docs page with several demos, a canvas with several stories. FixtureScope calls it; this is for code that sets up a page without React.
The fixture’s injections go in next to those already loaded. Injection ids carry the route of their view, so the fixtures of different views don’t clash. Where QLive isn’t initialized yet, this initializes it as initFixture() does. Otherwise the fixture has to fit the page:
- The page has to run on fixtures. graphql() and subscribeToTopic() ask isFixture(), which holds for the whole page, so a page can’t be live in one place and on a fixture in another.
- Its schema has to have the same types, or its data would convert by the wrong one without any sign of it. A reduced config, one without the schema, fits any; where one fixture has the full config and another the reduced one, the full one is kept.
- An injection id already loaded has to come with the same data. Two fixtures for one route can’t share a page.
The authentication, CSRF token, locale, translations and layouts stay those of the fixture that initialized the config.
Parameters
fixture |
the fixture |
Returns the config the page runs with
FixtureScope()
Section titled “FixtureScope()”function
declare function FixtureScope({ fixture, children }: FixtureScopeProps): JSX.Element;Runs the view below on the given fixture, at the fixture’s route. For whatever renders a view without startup(): an island on a static page, a Storybook story, a component test.
<FixtureScope fixture={sortingFixture}> <Sorting/></FixtureScope><FixtureScope fixture={filtersFixture}> <Filters/></FixtureScope>Any number of scopes can share a page, one per route: the fixture goes in next to the others with addFixture(), and the view reads its injections by the route a ViewRoute gives it. A fixture that doesn’t fit the page – another application’s, another recording for a route already there, a page running on a server – throws while rendering, for the nearest ErrorBoundary to show.
Two scopes with the same fixture share its documents: sort the view in one, and the other sorts with it.
FixtureScopeProps
Section titled “FixtureScopeProps”type
type FixtureScopeProps = { /** * The fixture the view below runs on, e.g. one recorded from a running application in dev mode */ fixture: QLiveFixture; children: ReactNode;};isFixture()
Section titled “isFixture()”function
declare function isFixture(): boolean;Returns true if QLive runs on a fixture, see initFixture().
A view runs the same either way and normally has no reason to ask. Where it shows something only a server makes work – a save button, say – this is how it can tell.
config()
Section titled “config()”function
declare function config(): QLiveConfig;QLiveConfig
Section titled “QLiveConfig”type
type QLiveConfig = { /** * Relative path of the QLive server */ contextPath: string; /** * GraphQL schema for this application */ schema: GraphQLSchema; /** * Domain meta information for the schema. * Contents vary with MetadataProvider configuration. QLive comes with `maxPageSize` and query configuration by type * which is transmitted here. */ meta: DomainMeta; /** * Every row of every catalog type, by type name, in the wire format; read them with catalogRows(). Empty in a * reduced config, and absent from a fixture recorded before there were catalogs. */ catalogs?: Record<string, Record<string, unknown>[]>; csrfToken?: CSRFToken; /** * Who is looking at this page. Not part of the config the server renders -- that one is per module and * shared by everyone it is served to -- but put here on arrival, the way the CSRF token is, because * this is where an application looks things up. */ authentication?: Authentication; /** * Language tag of the locale the page is served in, `"en"` or `"de-CH"`: the one of the locales the server * offers that the browser's language settings match best. Put here on arrival, like the authentication. */ locale?: string; /** * Cached set of the names of QueryDocument<T> equivalent types in the application */ queryDocumentTypes?: Set<string>; /** * Cached lookup for GraphQL types by name */ typesByName?: Map<string, GraphQLType>; /** * The component rendered in place of a view QLive could not produce -- a path no view answers, a view * module that failed to load. * * The one member of this config an application writes rather than reads. An initialized config always * carries one, so a caller can render it without a fallback of its own; assign your own in startup()'s * init hook, which runs after the config is initialized and before anything is rendered. * * await startup({ * views: import.meta.glob("./app/**\/*.tsx"), * init: async config => { * config.errorView = MyErrorPage * } * }) */ errorView?: ComponentType<ErrorViewProps>; /** * What i18n() looks its tags up in: a tag, `"Filter {0}"` or `"Foo.name"`, to its translation, which holds the * same placeholders. A tag without one renders as itself in brackets. The server sends the translations into the * page's locale with the bootstrap, and they are put here on arrival. An application changing or adding to them * does so in startup()'s init hook, like it does errorView. * * config.translations = {...config.translations, "Filter {0}": "{0} filtern"} */ translations?: Record<string, string>; /** * Widgets and decorators for the form fields the application doesn't name one for, matched by where the field is * and what it is, see WidgetOverride. Written by the application like errorView, in startup()'s init hook. * * config.widgets = [{fieldType: "Money", widget: MoneyInput}] */ widgets?: WidgetOverride[]; /** * The named layouts, which `<Form layout="name"/>` renders (namedLayout()). The server sends its own with the * bootstrap (its LayoutProviders), and they are put here on arrival, an empty map where there are none. The * application's join them in one namespace: added, changed or replaced in startup()'s init hook, like the * translations. * * config.layouts = {...config.layouts, fooCompact: {domainType: "Foo", nodes: [{field: "name"}]}} */ layouts?: Record<string, Layout>;};QLiveBoostrap
Section titled “QLiveBoostrap”type
type QLiveBoostrap = { /** * System config */ config: QLiveConfig | null; /** * CSRF-Token needed to POST stuff */ csrfToken: CSRFToken; /** * The current user */ authentication: Authentication; /** * Language tag of the locale the page is served in, the one of the locales the application offers that the * browser asked for. Absent from a fixture recorded before there were translations, like `translations`. */ locale?: string; /** * Tag to translation in that locale, which init() puts into the config. */ translations?: Record<string, string>; /** * Layout name to layout, which init() puts into the config. Absent from a page without the schema, and from a * fixture recorded before there were layouts. */ layouts?: Record<string, Layout>; /** * Injection data */ data: { [key: string]: InjectionSource; };};Entry-point boostrap data.
QLiveFixture
Section titled “QLiveFixture”type
type QLiveFixture = QLiveBoostrap & { /** * What the fixture holds and where it came from, for people: the recorder gives the time, location, login and * rows it recorded; a fixture written by hand or generated says what it is made of. */ description: string; /** * Route of the view the fixture was recorded on, as routeOf() gives it: "grid/sorting". The injection ids in * `data` start with it, and FixtureScope renders its view at it. A route rather than the location path, which * only resolves under the base of the application it was recorded in. */ route: string;};Bootstrap data a view runs on without a server, see initFixture().
A bootstrap like any other but for its query document injections, which hold every row instead of a page of them:
rows is the whole result, rowCount its length, and config the one the view injected with, so the view opens
on the page it opens on live.
Authentication
Section titled “Authentication”type
type Authentication = { /** * Login name of the current user */ login: string; /** * Roles of the current user. */ roles: string[]; /** * Id of the current user (Usually pointing to the app_user table) */ id: string;};Who the page is being served to, as AppAuthentication describes them on the Java side.
Anonymous is an answer and not an absence: a page served to nobody in particular carries the anonymous login, its role and its fixed id, so a reader never has to handle “not logged in” as a missing value.
CSRFToken
Section titled “CSRFToken”type
type CSRFToken = { /** * Name of the hidden input field carrying the token inside an HTML form POST */ param: string; /** * HTTP header that contains the token value for a fetch POST or similar. */ header: string; /** * CSRF token value */ value: string;};Cross-Site Request-Forging protection token handling.
DomainMeta
Section titled “DomainMeta”interface
interface DomainMetaDomain meta information from the server.
Declared as an interface, not a type alias, so that an application can extend it. The server-side io.github.qlivedev.qlive.graphql.meta.DomainMeta is an open map that every MetadataProvider bean adds its own addenda to, and declaration merging is the client-side equivalent: name the addenda your providers write once and they are typed at every place the application reads config().meta.
declare module "@qlivedev/qlive-ts" { interface DomainMeta { myAddendum: MyAddendumInfo[] }}DomainMeta.types
Section titled “DomainMeta.types”types: { [typeName: string]: DomainTypeMeta;Contains type names mapped to TypeMeta
DomainMeta.genericTypes
Section titled “DomainMeta.genericTypes”genericTypes: GenericTypeInfo[];DomainMeta.relations
Section titled “DomainMeta.relations”relations: RelationInfo[];DomainMeta.manyToMany
Section titled “DomainMeta.manyToMany”manyToMany: ManyToManyInfo[];DomainTypeMeta
Section titled “DomainTypeMeta”interface
interface DomainTypeMetaMeta data for a single GraphQL object type.
DomainTypeMeta.fields
Section titled “DomainTypeMeta.fields”fields?: { [fieldName: string]: DomainFieldMeta;Field meta data by field name. Absent if no provider wrote field meta data for this type.
DomainTypeMeta.meta
Section titled “DomainTypeMeta.meta”meta?: DomainTypeMetaProps;Type meta data. Absent if no provider wrote type meta data for this type.
DomainTypeMetaProps
Section titled “DomainTypeMetaProps”interface
interface DomainTypeMetaPropsType-level meta data properties, written server-side with DomainTypeMeta#setMeta.
Extend by declaration merging, see DomainMeta.
DomainTypeMetaProps.nameFields
Section titled “DomainTypeMetaProps.nameFields”nameFields?: string[];Names of the fields naming an instance of the type to a user, most significant first. Written by QLive’s NameFieldProvider.
A grid column of a relation and pick() show a row by these, so under vite dev a query selecting the type
without all of them logs a warning the first time it is used. Mutations are not checked.
DomainTypeMetaProps.uniqueKeys
Section titled “DomainTypeMetaProps.uniqueKeys”uniqueKeys?: UniqueKeyInfo[];The unique constraints of a table-backed type, the primary key first. Written by QLive’s UniqueKeyProvider from the jOOQ tables; unique indexes that are not constraints are not included.
DomainTypeMetaProps.queryConfig
Section titled “DomainTypeMetaProps.queryConfig”queryConfig?: QueryConfigDelta;What querying rows of this type looks like when nothing says otherwise: a page size, a sort order, a condition every query of the type carries. A delta, so a type naming only a page size leaves the rest at the defaults a query config has anyway.
Written server-side by QLive’s QueryConfigMetadataProvider, which an application registers as a MetadataProvider bean if it wants type-level defaults at all – absent everywhere in one that does not.
The server applies this to the queries a view injects with useInjection(), whose parameters are static and have no config to update. Nothing on the client applies it: read it where you build a query config from scratch and want it to start where the injected ones start.
DomainTypeMetaProps.maxPageSize
Section titled “DomainTypeMetaProps.maxPageSize”maxPageSize?: number;The largest page any query over rows of this type comes back with. Absent where the type sets no limit, which is also what a page size of 0 asks for.
Written server-side by QLive’s QueryConfigMetadataProvider and enforced there: a config asking for a larger page – or for all rows – is held to this, and the config that comes back on the document says so. Read it to keep a page size control from offering what the server will not give, not to enforce anything.
DomainTypeMetaProps.catalog
Section titled “DomainTypeMetaProps.catalog”catalog?: boolean;true for a catalog type: a closed set of rows, constant while the application runs, which the bootstrap ships whole (see catalogRows()). A picker filters them in the browser rather than asking the server. Written by QLive’s CatalogMetadataProvider.
DomainTypeMetaProps.merge
Section titled “DomainTypeMetaProps.merge”merge?: MergeTypeMeta;What this type declares about merging it: whether a conflict comes back to the view to resolve, which fields never count as one, and whether a non-overlapping change may merge silently.
Written server-side by QLive’s MergeMetadataProvider, which an application registers as a MetadataProvider bean if it declares any of this – absent everywhere in one that does not, and on every type that declared nothing.
Whether a type takes part in merging at all is not in here: that is the type having a “version” field, which both ends derive from the schema. Read it through the functions in merge/meta rather than off the map, so that “declared nothing” and “no such type” answer the same way.
UniqueKeyInfo
Section titled “UniqueKeyInfo”interface
interface UniqueKeyInfoOne unique constraint of a table-backed domain type.
UniqueKeyInfo.name
Section titled “UniqueKeyInfo.name”name: string;Name of the constraint in the database.
UniqueKeyInfo.fields
Section titled “UniqueKeyInfo.fields”fields: string[];Fields of the type the constraint covers, in constraint order.
UniqueKeyInfo.primary
Section titled “UniqueKeyInfo.primary”primary: boolean;Whether this is the primary key.
UniqueKeyInfo.nullable
Section titled “UniqueKeyInfo.nullable”nullable: boolean;Whether any of the fields is nullable. The database lets any number of rows hold NULL in a unique constraint, so such a constraint does not make rows distinct – a sort covering it can still have ties.
DomainFieldMeta
Section titled “DomainFieldMeta”interface
interface DomainFieldMetaField-level meta data properties, written server-side with DomainTypeMeta#setFieldMeta.
Extend by declaration merging, see DomainMeta.
DomainFieldMeta.computed
Section titled “DomainFieldMeta.computed”computed?: boolean;true if the field is a QLiveComputed-annotated property. Written by QLive’s ComputedMetadataProvider.
DomainFieldMeta.editable
Section titled “DomainFieldMeta.editable”editable?: boolean;false if a form shows the field without editing it, see defaultFieldMode(). Written by an application for columns only the server writes, such as created and modified stamps. A primary key, a computed field and the version field are not edited without it.
DomainFieldMeta.maxLength
Section titled “DomainFieldMeta.maxLength”maxLength?: number;The most characters a string field holds – or elements a list field, or entries a map field. Written by
QLive’s SizeMetadataProvider from the field’s @Size(max = n), which jOOQ generates from the column length, so
a table-backed field of limited length has it. Absent where nothing limits the field.
DomainFieldMeta.minLength
Section titled “DomainFieldMeta.minLength”minLength?: number;The fewest characters a string field holds – or elements a list field, or entries a map field. Written by
QLive’s SizeMetadataProvider from the field’s @Size(min = n). jOOQ never generates a minimum, so only a field
that was given one by hand has it.
DomainFieldMeta.precision
Section titled “DomainFieldMeta.precision”precision?: number;The most significant digits a BigDecimal or BigInteger field holds, for a BigDecimal on both sides of the point
together. Written by QLive’s DecimalMetadataProvider from the field’s @Column(precision = p), which jOOQ
generates for a numeric(p, s) column. Absent for an unconstrained numeric, and on every field that is
neither.
DomainFieldMeta.scale
Section titled “DomainFieldMeta.scale”scale?: number;The digits a BigDecimal field holds after the point. Written by QLive’s DecimalMetadataProvider from the field’s
@Column(scale = s), wherever it writes a precision – 0 then means the field holds integers. Never on a
BigInteger, whose scale is 0 by definition.
GenericTypeInfo
Section titled “GenericTypeInfo”type
type GenericTypeInfo = { /** * GraphQL Type name */ type: string; /** * TypeParameters as GraphQL type names */ typeParameters: string[]; /** * Full-qualified Java class name of the original generic class. Mostly useful as constant in the TS world */ genericType: "io.github.qlivedev.qlive.model.QueryDocument" | string;};Meta-information about types that are generic types on the Java side.
For example, io.github.qlivedev.qlive.model.QueryDocument
RelationInfo
Section titled “RelationInfo”type
type RelationInfo = { /** * Right side type of the relation */ targetType: string; /** * Generated GraphQL field of the left / source side of the relation */ leftSideObjectName?: string; /** * Generated GraphQL field of the right / target side of the relation */ rightSideObjectName?: string; /** * SourceField enum. Controlled the GraphQL fields on the left side */ sourceField: SourceField; /** * Target field enum. Controlled the GraphQL fields on the right side */ targetField: TargetField; /** * Left side type of the relation */ sourceType: string; /** * Meta tags */ metaTags: string[]; /** * Name of the relation */ id?: string; /** * Fields on the left side table forming the foreign key */ sourceFields: string[]; /** * Fields on the right side table the foreign key is pointing to */ targetFields: [string];};Describe a relation within the domain
ManyToManyInfo
Section titled “ManyToManyInfo”type
type ManyToManyInfo = { /** * Domain type of the link table, e.g. "BarLink" */ linkType: string; /** * End the declaration's first link column points at */ left: ManyToManyEnd; /** * End the declaration's second link column points at */ right: ManyToManyEnd; /** * Whether the fields of both ends can be written, i.e. whether a link row can be inserted from its two link * columns alone */ writable: boolean;};A many-to-many declared with configureManyToMany() or withManyToMany(): a link type whose rows each associate a row of one end with a row of the other.
ManyToManyEnd
Section titled “ManyToManyEnd”type
type ManyToManyEnd = { /** * Domain type of this end, e.g. "Bar" */ type: string; /** * Field of the link type pointing at this end, e.g. "barId" */ linkField: string; /** * Field on this end's type listing the rows on the other end, e.g. "bazes". Absent where the declaration * gave this end none. */ field?: string;};One end of a declared many-to-many: its type, the field of the link type pointing at it, and the field it gets listing the rows on the other end.
SourceField
Section titled “SourceField”type
type SourceField =/** * Ignore field for source type. */"NONE" |/** * Define a scalar GraphQL field for the key itself (e.g. fooId : string) */"SCALAR" |/** * Define an embedded object for the target */"OBJECT" |/** * Define a field for the key iteself *and* define an embedded object. * * This is useful in situations where you want the embedded object in some cases, but in others you * want to save one level of querying because all you need is the target id. */"OBJECT_AND_SCALAR";Source field configuration for a relation. Defines what fields to add for the relation on the side where the foreign key is.
TargetField
Section titled “TargetField”type
type TargetField =/** * Do nothing on target side. */"NONE" |/** * Assume the foreign key to represent a one-to-one relationship and embed a single object as back reference. */"ONE" |/** Assume the foreign key to represent a many-to-one relationship and embed a list of back references.*/"MANY";Target field configuration for a relation. Defines what fields should be added for the relation to the side the foreign key points to.
i18n()
Section titled “i18n()”function
declare function i18n(tag: string, ...args: string[]): string;Translates the given tag, filling in the given arguments.
The tag is the static text, the arguments what varies: i18n("Filter {0}", label). The translation comes from
config().translations, which the server fills for the locale the page is served in, and has the same
placeholders, {0} for the first argument, {1} for the second and so on, as in Java’s MessageFormat. A tag
without a translation renders as itself in brackets, its arguments appended: [Filter {0}: Name], so a missing
translation shows, and a tag with placeholders doesn’t read well with the arguments filled in.
The analysis records the tag of every call, which is what the server offers translations of. It records only a
tag that is static: a string literal, not a variable holding one, so write the tag out at the call. The one
exception is a tag named after the domain – a type, Type.field, Enum.VALUE – which the server offers for
every type with a table whether a call names it or not, and which QLive builds from the names it renders.
Parameters
tag |
translation tag |
args |
non-static arguments |
findRoot()
Section titled “findRoot()”function
declare function findRoot(id?: string): HTMLElement;Find the root container in a QLive template.
Parameters
id |
id of element to find, default is “root”, used by QLive for the root container |