Results and Errors
Overview
Every operation that can fail returns an OpResult. It is a discriminated union: ok: true carries the operation's data and the file changes it staged, ok: false carries a single BuilderError. Nothing throws for an expected failure — an unknown ref, a duplicate name, a config the engine cannot validate all come back as values.
Mutations are transactional. The engine applies the edit in memory, typechecks the project, and only writes when the typecheck passes; a failure rolls the transaction back and returns typecheck_failed, so a rejected operation leaves no partial edit on disk.
Branch on a Result
import { GetPageStructure } from "@antelopejs/interface-dms-builder";
const result = await GetPageStructure("/dashboard");
if (!result.ok) {
console.error(result.error.code);
return;
}
const { blocks, version } = result.data;
OpResult<T> is:
| Variant | Fields |
|---|---|
| success | ok: true, data: T, changes: FileChange[], warnings?: OpWarning[] |
| failure | ok: false, error: BuilderError |
OpResult defaults to OpResult<void>, which is what the operations that return nothing but a status use — ConfigurePage, RemoveBlock, RemoveField and the rest.
Reads that use the envelope — GetPageStructure, GetResourceStructure — return an empty changes array on success. The listing operations (ListPages, ListCategories, ListResources, ListQueryTemplates, GetCatalog) do not use the envelope at all: they return their value directly, and RefreshSourceIndex returns void.
Read the Staged Changes
changes is what the operation wrote, one entry per file.
| Field | Type | Description |
|---|---|---|
path | string | The file's absolute path. |
kind | "create" | "modify" | "delete" | What happened to the file. |
diff | string | A textual diff of the change. Empty for a deletion. |
A single operation usually touches more than one file — creating a page writes the page file and rewrites the barrel that exports it; creating a resource writes three files and rewrites a barrel.
import { CreateResource } from "@antelopejs/interface-dms-builder";
const result = await CreateResource({
name: "product",
fields: [{ name: "title", dataType: { $dataType: "string" } }],
});
if (result.ok) {
for (const change of result.changes) {
console.log(change.kind, change.path);
}
}
Handle Warnings
warnings is present only when an otherwise successful operation has something non-fatal to report. It is optional: check for it rather than assuming an array.
| Code | Raised when |
|---|---|
index_clamped | An AddBlock or MoveBlock index fell outside the sibling range and was clamped into it. |
datatype_fallback | A field's DataType is not one the builder maps, so it is handled as a string. |
data_not_dropped | DeleteResource removed the source but could not clear the resource's table data. |
import { AddBlock } from "@antelopejs/interface-dms-builder";
const result = await AddBlock({
page: "/revenue",
name: "total",
type: "KpiCard",
config: { title: "Total" },
index: 99,
});
if (result.ok) {
for (const warning of result.warnings ?? []) {
console.warn(warning.code, warning.message);
}
}
Handle Errors
BuilderError is a union discriminated by code. Each variant carries exactly what you need to act on it.
| Code | Payload | Meaning |
|---|---|---|
not_found | ref: string | The page, category, block, resource, field or query does not exist. |
duplicate_name | name: string, scope: DuplicateScope | The name is already taken within that scope. |
invalid_config | issues: ValidationIssue[] | The request's configuration did not validate. |
opaque_target | path: BlockPath | The target exists but the builder cannot rewrite it. |
unsupported | detail: string | The operation is valid but out of scope for the engine, and detail says why. |
referential_integrity | blockedBy: string[] | Something still depends on what you asked to delete. |
stale | ref: string, currentVersion: string | The file changed since the version you passed as expectedVersion. |
typecheck_failed | diagnostics: TypecheckError[] | The edit would not compile, and was rolled back. |
import { AddBlock } from "@antelopejs/interface-dms-builder";
const result = await AddBlock({
page: "/revenue",
name: "total",
type: "KpiCard",
config: { title: "Total" },
});
if (!result.ok) {
switch (result.error.code) {
case "duplicate_name":
console.error(`"${result.error.name}" is taken in ${result.error.scope}`);
break;
case "invalid_config":
for (const issue of result.error.issues) {
console.error(issue.pointer, issue.message);
}
break;
case "typecheck_failed":
for (const diagnostic of result.error.diagnostics) {
console.error(`${diagnostic.file}:${diagnostic.line}`, diagnostic.message);
}
break;
default:
console.error(result.error.code);
}
}
Duplicate Scopes
DuplicateScope names the namespace the collision happened in, which tells you what to rename:
| Scope | The name collided with |
|---|---|
page | A top-level block on the same page, or another page's route. |
siblings | Another child of the same container block. |
category | Another category. |
resource | Another resource, or another field on the same resource. |
query | A member of the page controller, a route path already served by the page, or a member of the resource's model. |
Validation Issues
ValidationIssue addresses a failure by JSON pointer into the request:
| Field | Type | Description |
|---|---|---|
pointer | string | A JSON pointer into the offending input, e.g. /where/0/op. Empty when the failure is not tied to one key. |
message | string | What is wrong, and often what to call to find the valid values. |
Typecheck Errors
TypecheckError is one TypeScript diagnostic from the in-memory typecheck gate:
| Field | Type | Description |
|---|---|---|
file | string | The file the diagnostic points at. |
line | number | The line number. |
message | string | The diagnostic message. |
Diagnostics are collected across the project, not just the file the operation edited, so a change that breaks a consumer elsewhere is caught before anything is written.
Make a Write Conditional
MutationOpts is the trailing options argument of version-checked mutations, not of every mutation.
| Option | Type | Description |
|---|---|---|
expectedVersion | string | The version the consumer last read. A mismatch fails with stale. |
The operations that edit something that already exists take it: ConfigurePage, DeletePage, AddBlock, ConfigureBlock, MoveBlock, RemoveBlock, DeleteResource, AddField, ConfigureField, RemoveField, AddQuery, ConfigureQuery and RemoveQuery. The three creations — CreatePage, CreateCategory and CreateResource — take no options argument, and neither do ConfigureCategory and DeleteCategory.
The version comes from the structure you read: PageStructure.version for a page operation, ResourceStructure.version for a resource one. Both are content hashes — of the page file, and of a resource's two generated files.
import {
ConfigureBlock,
GetPageStructure,
} from "@antelopejs/interface-dms-builder";
const read = await GetPageStructure("/revenue");
if (!read.ok) {
return;
}
const write = await ConfigureBlock(
"/revenue#total",
{ title: "Revenue" },
{ expectedVersion: read.data.version },
);
if (!write.ok && write.error.code === "stale") {
// `write.error.currentVersion` is what the file hashes to now:
// re-read the structure, re-decide, and retry.
}
Omitting expectedVersion writes unconditionally. Pass it whenever a human or another process may have touched the file between your read and your write.
Two operations extend MutationOpts with options of their own: ConfigureBlockOpts adds replace and meta, described in Page Operations, and DeleteResourceOpts adds keepData, described in Resources.