Drafts and Preview
Warning:
SetPageBlocksreplaces the whole block tree. Blocks you omit are removed. For one small change to an existing page, useConfigureBlockinstead.
Preview one card
With an existing /dashboard page, this read-only example returns a serialized KPI card. An editor can render preview.data.components.total; no source file changes.
import { PreviewLayout } from "@antelopejs/interface-dms-builder";
const preview = await PreviewLayout("/dashboard", {
blocks: [{ name: "total", type: "KpiCard", config: { title: "Total revenue" } }],
});
if (preview.ok) {
console.log(preview.data.components.total);
console.log(preview.data.degraded);
} else {
console.error(preview.error);
}
The per-block operations each save immediately. A visual editor usually holds edits locally and calls SetPageBlocks once, so the block tree and its queries either pass one typecheck together or remain unchanged.
Describe a Draft
PageDraft is a page as you want it written.
| Field | Type | Description |
|---|---|---|
page | Partial<EditablePageMeta> | Metadata to patch alongside the blocks. Omit to leave it alone. |
blocks | BlockDraft[] | The top-level blocks, in the order they should appear. |
queries | AddQueryInput[] | The queries the page should serve, written with the blocks. Omit to leave the page's queries alone. |
A block and the data it reads are one edit, so they are one write: the blocks, the model methods and the routes land in a single transaction with a single typecheck, or none of them land.
Omitting queries is not the same as passing an empty array. Omitted leaves whatever the page already serves untouched — which is what a caller written before queries joined the draft keeps doing. An empty array says the page should serve none, and removes the generated ones.
Two things a draft never destroys silently:
- A query edited past the grammar is left exactly as it is, whether the draft still lists it or not. The engine reports
query_opaqueand moves on, because a chain it cannot read is one a human owns. - A query the draft forgot is kept when a block on the page still reads its URL, reported as
query_kept. A draft that dropped a source a block depends on is a draft that would break that block, and keeping it is the recoverable side of the mistake.
import { SetPageBlocks } from "@antelopejs/interface-dms-builder";
await SetPageBlocks("/shop/board", {
blocks: [
{
name: "revenueChart",
type: "ChartCard",
config: {
title: "Revenue",
fetchUrl: "/dms/pages/shop/board/stats/revenue",
chart: { $block: { type: "ChartArea", config: { xaxisType: "category" } } },
},
},
],
queries: [
{
name: "revenue",
resource: "order",
template: "series",
params: { op: "sum", field: "amount", groupBy: "createdAt", bucket: "month" },
response: "card",
},
],
});
BlockDraft is one block:
| Field | Type | Description |
|---|---|---|
name | string | Static key (top-level) or .child() id (nested). Must be a valid identifier. |
type | string | A BlockTypeDescriptor.type from the catalog. Ignored when preserve is set. |
config | Record<string, unknown> | The factory's options. |
slot | string | The slot the block sits in. |
meta | Record<string, unknown> | Child metadata emitted with slot — { colSpan: 2 } on a grid child. |
controller | ResourceRef | Required for a controller-leading type such as TableView. |
children | BlockDraft[] | Nested blocks, in .child() order. |
preserve | boolean | Write the block back exactly as it stands on disk. |
Carry an Opaque Block Through a Write
A whole-tree write has to answer for blocks the builder can read but not rewrite: a CustomComponent, a block behind a computed expression, anything GetPageStructure reported with editable: false. preserve is that answer — the block is matched by name among its siblings and its source text is written back untouched.
To change only the page title, preserve every top-level block, including its children. This avoids reconstructing editable blocks from only part of their configuration:
import {
GetPageStructure,
SetPageBlocks,
} from "@antelopejs/interface-dms-builder";
const structure = await GetPageStructure("/dashboard");
if (structure.ok) {
const blocks = structure.data.blocks.map((block) => ({
name: block.name,
preserve: true,
}));
const result = await SetPageBlocks("/dashboard", {
page: { displayName: "Revenue" },
blocks,
}, {
expectedVersion: structure.data.version,
});
if (!result.ok) console.error(result.error);
}
When you do edit a block, supply its full draft, including its children, slot, meta and controller where applicable. Do not reduce a block tree to names and configuration objects: omitted children are deleted.
A preserve block whose name matches nothing on disk is rejected with invalid_config.
Write a Whole Page
import { GetPageStructure, SetPageBlocks } from "@antelopejs/interface-dms-builder";
const structure = await GetPageStructure("/dashboard");
if (!structure.ok) throw new Error(structure.error.code);
const result = await SetPageBlocks(
"/dashboard",
{
page: { displayName: "Revenue" },
blocks: [
{
name: "kpis",
type: "Grid",
config: { gap: "1rem" },
children: [
{
name: "row",
type: "GridRow",
children: [
{
name: "revenue",
type: "KpiCard",
config: { title: "Revenue", valueFormat: "currency" },
meta: { colSpan: 2 },
},
],
},
],
},
],
},
{ expectedVersion: structure.data.version },
);
if (!result.ok) throw new Error(result.error.code);
Read the version immediately before preparing the draft. If the write returns stale, read the page again and review the other editor's changes before retrying.
On success the result carries the staged changes and the page's new version, ready to be passed as the next expectedVersion.
Blocks absent from the draft are removed. Static members that are not blocks are left where they are, and named imports nothing references any more are dropped, so a page shrinks cleanly rather than accumulating dead imports.
$expr is refused anywhere in a draft: a whole-tree write stays structured, and arbitrary code would defeat the guarantee. $dataType, $ref and $block are all accepted — see Value Encodings.
Read the Failures
SetPageBlocks validates the whole draft before touching anything, so one call reports every problem at once, each addressed by a JSON pointer into the draft:
| Pointer | Cause |
|---|---|
/blocks/1/name | Not a valid identifier, or a duplicate among its siblings. |
/blocks/2/type | Unknown block type, or a child a container does not accept. |
/blocks/0/controller | A controller-leading type without one, or a controller on a type that takes none. |
/blocks/3/config/title | $expr inside a config value. |
Beyond invalid_config, the write fails the same way the per-block operations do: stale when expectedVersion no longer matches, typecheck_failed when the emitted source does not compile — and then nothing is written. See Results and Errors.
Preview a Draft
PreviewLayout serializes a draft the way the runtime would, by calling the same dms-base factories the emitted source would call. Nothing is typechecked and nothing reaches disk, so it is cheap enough to run on every edit.
const preview = await PreviewLayout("/dashboard", draft);
if (preview.ok) {
const grid = preview.data.components.kpis; // ComponentPreview
}
PageLayoutPreview holds:
| Field | Type | Description |
|---|---|---|
components | Record<string, ComponentPreview> | The serialized tree, keyed by top-level block name — the shape the DMS frontend renders. |
degraded | BlockPath[] | Blocks the preview could not build faithfully. |
A block lands in degraded when the preview cannot reach what the running page holds: a controller-leading block such as TableView (the DataAPI class lives in the app, not in the builder), a $ref, an $expr, a preserve block (which the draft does not describe), or a factory that threw. Render the last known-good layout for those rather than trusting the preview.
Preview a Query
PreviewLayout says what the page will look like; PreviewQuery says what its blocks will be showing. It runs a query the caller has not saved and answers what its route would, so a source can be configured against real data before anything is written.
import { PreviewQuery } from "@antelopejs/interface-dms-builder";
const result = await PreviewQuery({
query: {
name: "revenue",
resource: "order",
template: "series",
params: {
op: "sum", field: "amount", groupBy: "createdAt", bucket: "month",
where: [
{ field: "createdAt", op: "ge", value: { $param: { name: "from" } } },
{ field: "createdAt", op: "lt", value: { $param: { name: "to" } } },
],
},
},
args: { from: "2026-01-01T00:00:00.000Z", to: "2026-09-01T00:00:00.000Z" },
});
if (result.ok) {
console.log(result.data.body); // { series: [{ x: 202601, y: 1250 }, …] }
console.log(result.data.truncated); // whether it stopped reading early
}
| Field | Type | Description |
|---|---|---|
query | AddQueryInput | What to run. Validated the same way AddQuery validates it. |
args | Record<string, unknown> | Values the route would receive, by the name it exposes them under. A bound value with nothing supplied is refused rather than compared against nothing. |
tenant | string | Whose rows to read, for a resource in a per-tenant schema. |
What comes back is the route's own response body, not a rendering of it: body is built by the same helper the generated route would call, and output and response report it the way GetPageStructure reports the saved query. Ask for response: "card" and the preview answers a ChartCardData — the headline figure included — so a block can be handed it directly. response is set only when a helper arranged the body, which is the same condition read-back reports it under; a plain { value } or { series } leaves it absent. An arrangement the installed DMS does not publish is refused as unsupported rather than answered as bare points, because saving that query would fail its typecheck on the very same helper.
Read-only by construction: a query is filters, a grouping and a measure, and there is no way to spell a write in one. It stops at 500 points and sets truncated, because a chart that quietly lost its tail is worse than one that reports it. A database that refuses comes back as a failed OpResult, not as a rejection.
Next Steps
- A Chart Without Code - The journey these calls add up to
- Results and Errors - Branch on
OpResultand handlestale - Catalog - The block types, their schemas and their defaults