Escape
HomeDocsReportSchema

ReportSchema

The document shape. Every object with fixed keys is a closed set, so an unknown key anywhere is a definition error.

The document

Field Type Meaning
data string Required. The JSONPath selecting the rows the report walks.
params Record<string, JsonValue> Constants the definition declares, read as $.params.
where `=${string}` Drops rows that fail it, before aggregates.
sort SortKey[] Orders the whole set, applied left to right.
take number Keeps this many rows after the sort. Integer >= 1. Dropped rows leave aggregates and runners too.
aggregates Record<string, ReducerSpec> Report-level aggregates, injected bare under $.
run Record<string, ReducerSpec> Running accumulators, read as run.<name> on detail rows.
style ReportStyleDeclarations The report default. family and size only.
header Item[] or { height, items } The report header band. The object form pins a box from the page top.
empty Item[] Replaces the whole body when no rows remain.
columns number Flows the body in this many page columns. Integer >= 2, and no nesting.
groups Group[] Group bands, outermost first.
detail Item[] or TableDetail The per-row band.
footer Item[] The report footer band.
page PageBands Furniture paginated targets draw on every page.

Group

signature
interface Group {
  name: string
  by: `=${string}`
  break?: 'page'
  reset?: 'page'
  columns?: number
  sort?: SortKey[]
  take?: number
  aggregates?: Record<string, ReducerSpec>
  run?: Record<string, ReducerSpec>
  header?: Item[]
  footer?: Item[]
}

name is the handle its bands and everything nested inside them read. It must be a plain identifier, unique across the document, and can’t be @, $, run, loop or page. A group aggregate can’t take the name key.

break: "page" starts each instance on a fresh page. reset: "page" also restarts the page numbering.

TableDetail

signature
interface TableDetail {
  header?: TableHeaderBox
  row?: TableRow
  columns: [TableColumn, ...TableColumn[]]
  total?: TotalBlock
}

TableColumn

signature
interface TableColumn extends Cell {
  header?: string | TableHeader
  width?: number
}

width is a literal percentage from 0 to 100, and it’s the border-box share.

TableHeader, TableRow, TableHeaderBox

signature
interface TableHeader { value: CellValue; style?: StyleDeclarations; span?: number }
interface TableRow { visible?: ExpressionValue<boolean>; style?: StyleDeclarations }
interface TableHeaderBox { style?: StyleDeclarations }

A row’s style resolves onto that row’s cells on every target.

TotalBlock, TotalRow, TotalCell

signature
interface TotalBlock extends TableRow { rows: [TotalRow, ...TotalRow[]] }
interface TotalRow {
  cells: [TotalCell, ...TotalCell[]]
  visible?: ExpressionValue<boolean>
  style?: StyleDeclarations
}
type TotalCell = Cell & { span?: number }

What every total row shares goes on the block. Each row must cover the columns exactly once, counting span.

Items

Cell and TextItem

signature
interface Cell {
  value: CellValue
  visible?: ExpressionValue<boolean>
  style?: StyleDeclarations
}
interface TextItem extends Cell { type: 'text' }
type CellValue = string | StyledRun[]
interface StyledRun {
  value: string
  style?: RunStyleDeclarations
}

A cell value is one template string or a non-empty array of styled runs. A cell written as one string is one run, so a one-run array says exactly what that string says. A run's style is literal only, never an expression, and a run has no visible, since {{#if}} already conditions its text. A CR, LF, or CRLF in the evaluated text is a hard line break.

ImageItem

signature
interface ImageItem {
  type: 'image'
  source: `=${string}`
  fit?: 'natural' | 'width'
  alt?: string
  visible?: ExpressionValue<boolean>
  style?: ImageStyleDeclarations
}

source must be an expression resolving to PNG or JPEG bytes. A nullish source skips the item, so an optional logo needs no guard. style covers background, align, and the box and flow spacing, and nothing else. alt is one template string, the one cell value the run form doesn’t reach.

SplitItem

signature
interface SplitItem {
  type: 'split'
  slots: [SplitSlot, SplitSlot, ...SplitSlot[]]
  visible?: ExpressionValue<boolean>
  style?: StyleDeclarations
}

Two slots at minimum. A slot is a text or image item plus an optional width percentage, and it takes no flow spacing. Splits don’t nest, and neither appears in a table or total cell.

Item

signature
type Item = TextItem | ImageItem | SplitItem

PageBands

signature
interface PageBands {
  header?: Item[]
  footer?: Item[]
  margin?: number
}

margin is the one piece of page geometry a document may declare. Declaring it here and on the target too is a render error.

SortKey

signature
interface SortKey {
  by: `=${string}`
  dir?: 'asc' | 'desc'
}

dir defaults to "asc". The sort is stable. Keys compare with JavaScript ordering, so keep a key one type where the order matters.

Supporting types

signature
type ExpressionValue<T = unknown> = T | `=${string}`
type ReducerSpec = string
type JsonValue =
  | null | boolean | number | string
  | JsonValue[]
  | { [key: string]: JsonValue }

A leading = on any property other than a cell value makes it an expression. params accepts JSON values and nothing else, so a Date, a Map, a NaN or an Infinity is a definition error.

Every field a definition may hold.

npm install quario
Getting started
© 2026 quario · KvK 61815977