Your domain is one thing. It starts in the database and reaches the browser without being translated on the way.
Defined once
Section titled “Defined once”A type is defined once, and everything downstream knows it – the database, the Java code, the TypeScript in the browser. Nothing is hand-mapped between the two sides. The common language is a GraphQL schema, and you do not write it: it comes from your tables and your server code.
Filters are the same story. One condition, written once, runs as SQL against the database, as a predicate over Java objects, or in the browser – in the semantics of wherever it lands, rather than a lowest common denominator.
The grid below sorts and filters by a column no table has, the sum of two others:
field("numA").add(field("numB"))Running against an actual QLive server, that expression would be translated into SQL running on the database.
This page however has no such server, it just runs on Github Pages, a static file host. So here we reuse a method normally meant for testing or Storybook: We record the injections of a live example and the grid here then runs on recorded rows. It evaluates the recorded rows against the same current filter expression transformed into a JavaScript function. From the component’s perspective, nothing changes.
Click the sum’s header, or try limiting the date range.
import { dateRangeFilter, DataGrid, FilterInput, operatorFilter, useFilters, useInjection } from "@qlivedev/qlive-ts";import { field } from "@qlivedev/qlive-ts/filter";import { Q_QuuxList, Q_QuuxListResult } from "./Q_QuuxList";import filterAsTitle from "../../util/filterAsTitle";
/** * The sum the computed column shows, as the server sorts by it. */const SUM = field("numA").add(field("numB"));
/** * The search form's one filter: the days the Quux was created on. */const SEARCH = [ {field: "created", filter: dateRangeFilter("Timestamp")}];
/** * A column computed from two fields, sorted by the expression that computes it, under a date range form. * * The document starts sorted by the sum, largest first. A click on the sum's header sorts by it ascending, the next * one descending again -- an expression toggles like a field. The sum filters like a field too: its filter compares * the expression the column sorts by, so `between` gives a range of sums. The form narrows the rows to a range of * days, from or until alone for a range open at the other end. */export default function Sum(){ // written out rather than SUM.desc(): the build reads this call, and it runs FilterDSL builders but does not // follow a variable to its value const quuxes: Q_QuuxListResult = useInjection(Q_QuuxList, { config: {sortFields: [field("numA").add(field("numB")).desc()]} });
const search = useFilters(quuxes, "search", SEARCH); const [created] = search.columns;
return ( <div className="grid-example"> <h1>Sorted by a sum</h1> <form className="grid-example-search" onSubmit={ ev => ev.preventDefault() }> <label> Created from, until <span className="grid-example-range"> <FilterInput column={ created }/> </span> </label> <button type="button" className="btn" disabled={ !search.active } onClick={ () => search.reset() }> Clear search </button> </form> <DataGrid doc={ quuxes } headerTitle={ filterAsTitle(quuxes.config)} columns={ [ "name", {field: "numA", filter: operatorFilter("between", "Int")}, {field: "numB", filter: operatorFilter("between", "Int")}, // computed in the browser, sorted and filtered on the server by the same sum { heading: "numA + numB", render: row => row.numA + row.numB, sort: SUM, filter: operatorFilter("between", "Int") }, // the form filters it already {field: "created", nowrap: true, filter: false} ] } /> </div> );}The data is already there
Section titled “The data is already there”A view’s query is a static declaration in that shared language, so the server can tell what a page needs before the page exists:
import {useInjection} from "@qlivedev/qlive-ts";export default function Home() { const foos = useInjection(Q_Foo); // ...}It runs the query and puts the result in the page. Nothing fetches it, and there is no loading state to render. Components get simpler for it, and their tests more so: there is no fetch to mock and nothing to wait for.
It does the same job as React Server Components, but with a Java server and a declarative way to express data needs.
Requirements
Section titled “Requirements”- Java 25
- pnpm
- vite
- React 18
- Spring Boot
- jOOQ
Where to look
Section titled “Where to look”Start with the Overview, which explains what the framework does and how a page reaches the browser.
The rest is grouped by what you came for:
- Explanation – what QLive does and why it is built that way. Meant to be read through, in the order it is in.
- How-to guides – one task at a time: wiring up a Spring application, refreshing the schema after a domain change, adding an entry point.
- Reference – the application layout, and the artifacts the codegen writes. Meant to be looked up, not read.
- API – every export of
@qlivedev/qlive-ts, generated from the declarations themselves.