[data-reveal]{opacity:1!important;transform:none!important}
Page Operations
Interface reference

Page Operations

Overview

Page operations cover everything that lives in a page file: the pages themselves, the categories they hang from, and the block tree inside each page. Reads (ListPages, ListCategories, GetPageStructure) tell you what is there; mutations (CreatePage, AddBlock, MoveBlock, …) rewrite the source and return the staged FileChange list.

Every mutation returns an OpResult, so branch on ok before touching data. The refs and shapes these operations trade in are described in Addressing and Structure; the errors and warnings they return are described in Results and Errors.

List Pages and Categories

ListPages and ListCategories are plain reads: they return arrays, not an OpResult.

import {
  ListCategories,
  ListPages,
} from "@antelopejs/interface-dms-builder";

const pages = await ListPages();
const categories = await ListCategories();

for (const page of pages) {
  console.log(page.ref, page.displayName, page.filepath);
}

Read a Page's Structure

GetPageStructure(ref) returns the page's metadata, its block tree and its queries, wrapped in an OpResult<PageStructure>. Being a read, its changes array is empty.

import { GetPageStructure } from "@antelopejs/interface-dms-builder";

const result = await GetPageStructure("/dashboard");
if (!result.ok) {
  throw new Error(`cannot read /dashboard: ${result.error.code}`);
}

const { page, blocks, queries, version } = result.data;

Keep version: passing it as expectedVersion on the next mutation turns the write into a conditional one.

Refresh the Source Index

The builder keeps an index of the project's source. Mutations invalidate it themselves, so you only need RefreshSourceIndex after something else changed the files — a hand edit, a checkout, a generator run.

import { RefreshSourceIndex } from "@antelopejs/interface-dms-builder";

await RefreshSourceIndex();                       // source index and catalog
await RefreshSourceIndex({ page: "/dashboard" }); // source index only

RefreshScope has a single field:

OptionTypeDescription
pagePageRefKeep the cached catalog. Omit the scope entirely to drop the catalog as well.

The scope does not narrow the source index: it is invalidated in full either way, and naming a page only spares the catalog cache. Pass a page when you know the change was confined to page source and left the component catalog alone — it saves rescanning the catalog, not the project.

Invalidating is not rescanning. The call only drops the cache; the cost lands on the next read, which rescans the project in full and memoizes the result. Two refreshes in a row therefore cost nothing extra, but refreshing before every read costs a full scan every time. The operation returns void, not an OpResult.

Create a Page

CreatePage(input) writes a new page file, wires it into the nearest barrel, and returns the ref it assigned along with the file it created.

import { CreatePage } from "@antelopejs/interface-dms-builder";

const result = await CreatePage({
  name: "revenue",
  displayName: "Revenue",
  category: "pages",
  icon: "i-ph-chart-line",
  description: "Monthly revenue overview",
});
if (result.ok) {
  console.log(result.data.ref, result.data.filepath);
}

CreatePageInput:

OptionTypeDescription
namestringThe page's route id/slug. Required. A duplicate fails with duplicate_name.
displayNamestringThe label shown in the menu. Required.
categoryCategoryRefThe category to attach to. Required, and it must already exist.
iconstringIcon name.
ordernumberSort order within the category.
descriptionstringThe page's description.
permissionPagePermissionThe permission guarding the page.
dirstringExplicit folder, relative to the src root, overriding the default category-relative placement.

Without dir, the page lands at <category-folder>/<name>/page.ts when the category is declared in the project's source and has a barrel beside it, and at src/<name>.ts otherwise. With dir, it lands at src/<dir>/<name>/page.ts. Either way the nearest barrel above the file is wired, keeping a relative import chain up to src/index.ts. dir must stay inside the src tree; a dir that escapes it, or a project with no barrel to wire, fails with unsupported.

import { CreatePage } from "@antelopejs/interface-dms-builder";

await CreatePage({
  name: "revenue",
  displayName: "Revenue",
  category: "pages",
  dir: "reporting", // → src/reporting/revenue/page.ts
});

Configure a Page

ConfigurePage(ref, patch, opts?) patches the page's metadata in place. The patch is a Partial<EditablePageMeta>; keys you leave out are untouched, and a key set to undefined is removed from the page's options.

import { ConfigurePage } from "@antelopejs/interface-dms-builder";

const result = await ConfigurePage("/revenue", {
  displayName: "Monthly revenue",
  order: 10,
  hidden: false,
});

Move a Page Between Categories

A page's category decides its route, so patching category moves the page rather than editing a field. ConfigurePage rewrites the category option and its import, moves the file to the directory beside the new category — where CreatePage would have put it — and moves the registration with it, so each directory's barrel still lists what it holds. Relative imports inside the page are re-anchored to its new location.

The result carries the page's new ref, and a route_changed warning naming the old one. Nothing redirects the old route: links and bookmarks to it break.

import { ConfigurePage } from "@antelopejs/interface-dms-builder";

const moved = await ConfigurePage("/form/revenue", { category: "pages.reports" });
if (moved.ok) {
  console.log(moved.data.ref); // "/reports/revenue"
  for (const warning of moved.warnings ?? []) {
    console.log(warning.code, warning.message); // "route_changed", …
  }
}

The move is refused when the target category is unknown (not_found), when it is built-in or not declared in source so a page cannot reference it (unsupported), or when the destination already holds a page of that name (duplicate_name). A page whose options are not a plain object literal fails with unsupported too — the builder rewrites source, so it needs a literal to rewrite.

opts is a MutationOpts, so expectedVersion makes the write conditional.

Delete a Page

DeletePage(ref, opts?) removes the page file and unwires every import of it.

import { DeletePage } from "@antelopejs/interface-dms-builder";

const result = await DeletePage("/revenue", { expectedVersion: version });
if (!result.ok && result.error.code === "stale") {
  // Re-read the page and decide again.
}

Create a Category

CreateCategory(input) declares a new category and registers it in the root barrel.

import { CreateCategory } from "@antelopejs/interface-dms-builder";

const result = await CreateCategory({
  name: "reports",
  displayName: "Reports",
  parent: "pages",
  icon: "i-ph-folder",
  order: 3,
});
// result.data.ref === "pages.reports"

CreateCategoryInput:

OptionTypeDescription
namestringThe category's id segment. Required. A duplicate fails with duplicate_name.
displayNamestringThe label shown in the menu. Required.
parentCategoryRefThe parent category. Omit for a category under the root pages.
iconstringIcon name.
ordernumberSort order among siblings.

A parent that does not exist fails with not_found.

Configure a Category

ConfigureCategory(ref, patch) patches an existing category. It takes no options argument.

import { ConfigureCategory } from "@antelopejs/interface-dms-builder";

await ConfigureCategory("pages.reports", {
  displayName: "Reporting",
  order: 1,
});

Two refusals, both unsupported: re-parenting (a patch carrying parent), and a category that is built in or otherwise not declared in editable source.

Delete a Category

DeleteCategory(ref) removes the category declaration, and deletes its file when nothing but imports is left in it.

import { DeleteCategory } from "@antelopejs/interface-dms-builder";

const result = await DeleteCategory("pages.reports");
if (!result.ok && result.error.code === "referential_integrity") {
  console.log("still in use by", result.error.blockedBy);
}

A category that still holds pages or child categories fails with referential_integrity, and blockedBy lists the refs standing in the way. Empty it first.

Add a Block

AddBlock(input, opts?) emits a factory call into the page and returns the BlockPath of the new block.

import { AddBlock } from "@antelopejs/interface-dms-builder";

const result = await AddBlock({
  page: "/revenue",
  name: "total",
  type: "KpiCard",
  config: {
    title: "Total revenue",
    icon: "i-ph-currency-eur",
  },
});
if (result.ok) {
  console.log(result.data.path); // "/revenue#total"
}

AddBlockInput:

OptionTypeDescription
pagePageRefThe page to add to. Required.
namestringThe block's name, unique among its siblings. Required. A duplicate fails with duplicate_name.
typestringA BlockTypeDescriptor.type from the catalog. Required. An unknown type fails with unsupported.
configRecord<string, unknown>The factory's options object. Required.
parentBlockPathThe container to nest under. Omit to add a top-level block.
slotstringThe slot within the parent container.
indexnumberPosition among siblings. Defaults to appending.
metaRecord<string, unknown>Child metadata emitted alongside slot in the third argument of .child(), such as { colSpan: 2 }. Ignored for top-level blocks.
controllerResourceRefThe resource whose DataAPI class is the leading argument, for controller-leading block types.

config is plain JSON by default; values that are not literals — a DataType instance, a reference to a generated class — use the sentinel encodings in Value Encodings.

Nesting requires an editable container: a parent that resolves to an opaque block fails with opaque_target, and one that does not resolve at all fails with not_found. An out-of-range index is clamped, and the result carries an index_clamped warning.

Controller-leading types (catalog controllerArg: true, e.g. TableView) require controller; passing it for any other type is rejected. Both mismatches fail with invalid_config.

import { AddBlock } from "@antelopejs/interface-dms-builder";

await AddBlock({
  page: "/products",
  name: "table",
  type: "TableView",
  controller: "product", // emitted as the imported productDataAPI class
  config: { caption: "Products" },
});

Configure a Block

ConfigureBlock(path, patch, opts?) rewrites a block's options object. By default the patch merges: listed keys are replaced, unlisted keys survive, and a key set to undefined is removed.

import { ConfigureBlock } from "@antelopejs/interface-dms-builder";

await ConfigureBlock("/revenue#total", {
  title: "Revenue (net)",
  description: undefined, // removes the option
});

ConfigureBlockOpts extends MutationOpts:

OptionTypeDescription
replacebooleanReplace the whole options object instead of merging into it.
expectedVersionstringThe PageStructure.version you last read. A mismatch fails with stale.
metaRecord<string, unknown>Patch child metadata (slot, colSpan, …). A key set to undefined is removed. Rejected for a top-level block, which has no .child() call.

Read the version before changing an existing child. Here /revenue#grid/total must already be a child of the page's grid:

import { ConfigureBlock, GetPageStructure } from "@antelopejs/interface-dms-builder";

export async function resizeRevenueCard() {
  const read = await GetPageStructure("/revenue");
  if (!read.ok) return read;

  return ConfigureBlock(
    "/revenue#grid/total",
    { title: "Revenue" },
    { expectedVersion: read.data.version, meta: { colSpan: 2 } },
  );
}

An opaque block fails with opaque_target: the builder can read it but not rewrite it.

Move a Block

MoveBlock(path, dest, opts?) moves a child block within its container or into another one.

import { MoveBlock } from "@antelopejs/interface-dms-builder";

await MoveBlock("/revenue#grid/row/total", {
  parent: "/revenue#grid/footer",
  index: 0,
});

MoveDest:

OptionTypeDescription
parentBlockPathThe destination container.
slotstringThe slot within the destination container.
indexnumberPosition among the destination's children. Required.

The move is a child-into-container operation: the path must address a nested block, and dest.parent must name a container. Anything else — a top-level block, a destination without a parent — fails with unsupported. An out-of-range index is clamped with an index_clamped warning, and a destination that is opaque or missing fails with opaque_target or not_found.

Remove a Block

RemoveBlock(path, opts?) deletes a block. A top-level block is removed as a static member; a nested one is dropped from its parent's .child() chain.

import { RemoveBlock } from "@antelopejs/interface-dms-builder";

const result = await RemoveBlock("/revenue#grid/row/total");
if (result.ok) {
  console.log(result.changes.map((change) => change.path));
}

Next Steps

  • Resources - Generate the tables and DataAPI controllers a page reads from
  • Drafts and Preview - Write a page's whole block tree in one transaction