Resources
Overview
A resource is the data half of a DMS feature: a database Table, a DataController exposing it over HTTP, and the model that binds them. CreateResource writes all of it into a <name>/ folder — database.ts, data-api.ts and an index.ts barrel — and wires the nearest barrel above so the project keeps a relative import chain to src/index.ts.
You describe a resource in semantic terms: each field carries a DataType and a set of aspects such as searchable or sortable, and the engine maps those to the paired decorator stacks on the table and the controller. You never write a decorator yourself, and you never name one in a request.
A resource is also what a controller-leading block points at: AddBlock with controller: "<ref>" emits the resource's DataAPI class as the block's leading argument, as described in Page Operations.
Address a Resource and a Field
ResourceRef is the resource's name in kebab form — "product", "order-line". It is what CreateResource returns, what ListResources reports, and what every other resource operation takes.
FieldPath addresses one field within a resource: <resourceRef>#<fieldName>, e.g. product#price. AddField returns the path it created.
import type {
FieldPath,
ResourceRef,
} from "@antelopejs/interface-dms-builder";
const resource: ResourceRef = "product";
const field: FieldPath = "product#price";
Create a Resource
import { CreateResource } from "@antelopejs/interface-dms-builder";
const result = await CreateResource({
name: "product",
displayName: "Product",
fields: [
{
name: "title",
dataType: { $dataType: "string" },
label: "Title",
required: true,
searchable: true,
sortable: true,
},
{
name: "price",
dataType: { $dataType: "number" },
sortable: true,
filterable: true,
},
],
});
if (result.ok) {
console.log(result.data.ref, result.data.files.dataApi);
}
CreateResourceInput:
| Option | Type | Description |
|---|---|---|
name | string | The resource name. Required, and it must contain at least one alphanumeric character. A duplicate fails with duplicate_name. |
fields | FieldSpec[] | The resource's fields. Required. |
displayName | string | A human-readable name for the resource. |
schema | string | The database schema. Defaults to the DMS core schema, CORE_SCHEMA_NAME (dms-core). |
seeds | Record<string, unknown>[] | Rows to seed as a fixture. Each row must supply its own _id. |
location | ResourceLocation | Where the generated folder goes. Omit for src/<name>/. |
routes | ResourceRoute[] | Which DataAPI routes to expose. Omit for the full set. Must be non-empty when provided. |
The names the engine derives from name are predictable: product yields a Product table class, a ProductModel, a productDataAPI controller on the route /api/product, and a products table.
Describe a Field
FieldSpec is a FieldAspects plus the field's name. The name is immutable — renaming a field is RemoveField followed by AddField.
| Option | Type | Description |
|---|---|---|
name | string | The field name. Rejected with invalid_config when it is one of the catalog's reservedFieldNames. |
dataType | DataTypeValue | The field's DataType, e.g. { $dataType: "string" }. Required. |
label | string | Column header label. Defaults to the field name. |
listable | boolean | Selected and listed by the DataAPI. Default true. |
searchable | boolean | Included in full-text search. |
sortable | boolean | Sortable in the table view. |
filterable | boolean | Column-filterable in the table view. |
access | "read" | "readwrite" | Field access mode. Default "readwrite". |
required | boolean | Whether the field is required. Default false, which makes the field optional. |
indexed | boolean | Whether the table column is indexed. |
order | number | Display order of the column. |
dataType takes a DataTypeValue sentinel, not a bare string; the ids come from the catalog's dataTypes. See Value Encodings and Catalog.
Choose the Exposed Routes
ResourceRoute names one group of DataAPI endpoints. Passing a subset lets a resource serve only what it needs — a singleton edit form wants ["get", "edit"] and nothing else. An omitted route's HTTP endpoint is simply not registered.
| Route | What it exposes |
|---|---|
list | The paginated listing (list + count). |
get | Read a single row. |
create | Create a row. |
edit | Update a row. |
delete | Delete a row. |
select | Option lists for relation pickers that reference this resource. |
archive | Soft delete and undo (archive + restore). |
export | The export endpoints (exportStart / exportStatus / exportDownload). |
import { CreateResource } from "@antelopejs/interface-dms-builder";
await CreateResource({
name: "site-settings",
fields: [{ name: "title", dataType: { $dataType: "string" } }],
routes: ["get", "edit"],
});
An empty array, or a name that is not one of the eight, fails with invalid_config.
Place the Generated Files
ResourceLocation decides which folder the <name>/ folder lands in. The nearest ancestor barrel to the resolved folder is the one that gets wired, with the root barrel as the fallback.
| Form | Where the resource lands |
|---|---|
| omitted | src/<name>/ |
"reporting/data" | src/reporting/data/<name>/ — an explicit folder relative to the src root, which must stay inside the src tree. |
{ category: "pages.reports" } | The source folder of that category, mirroring page placement. A root category falls back to the src root. |
{ page: "/revenue" } | Inside that page's feature folder, so the page imports ./<name>/data-api. |
import { CreateResource } from "@antelopejs/interface-dms-builder";
await CreateResource({
name: "revenue-entry",
fields: [{ name: "amount", dataType: { $dataType: "number" } }],
location: { page: "/revenue" },
});
A dir string that escapes the src tree, an unknown category and an unknown page all fail with invalid_config.
Add, Configure and Remove a Field
AddField(ref, field, opts?) appends a field to both generated files and returns its FieldPath.
import { AddField } from "@antelopejs/interface-dms-builder";
const result = await AddField("product", {
name: "sku",
dataType: { $dataType: "string" },
label: "SKU",
indexed: true,
searchable: true,
});
if (result.ok) {
console.log(result.data.path); // "product#sku"
}
A name already present on the table or the controller fails with duplicate_name; a reserved name fails with invalid_config.
ConfigureField(path, patch, opts?) reads the field's current aspects, merges your Partial<FieldAspects> over them, and rewrites both declarations from the result. Aspects you leave out keep their current value.
import { ConfigureField } from "@antelopejs/interface-dms-builder";
await ConfigureField("product#price", {
filterable: true,
order: 2,
});
A field whose decorator stack the builder could not reverse — a hand-edited one, or one carrying a decorator the builder does not know — is opaque, and configuring it fails with opaque_target.
RemoveField(path, opts?) drops the field from both files.
import { RemoveField } from "@antelopejs/interface-dms-builder";
await RemoveField("product#sku");
All three take a MutationOpts, whose expectedVersion is checked against the resource's version — a hash of both generated files.
List Resources
ListResources() returns a ResourceSummary[] directly, without an OpResult wrapper.
| Field | Type | Description |
|---|---|---|
ref | ResourceRef | The resource's ref. |
className | string | The generated table class name. |
tableName | string | The database table name. |
route | string | The DataAPI's base route. |
fieldCount | number | How many fields the resource has. |
files | { database: string; dataApi: string } | The two source files. |
import { ListResources } from "@antelopejs/interface-dms-builder";
const resources = await ListResources();
const refs = resources.map((resource) => resource.ref);
Read a Resource's Structure
GetResourceStructure(ref) returns an OpResult<ResourceStructure>. Like the other reads, its changes array is empty.
import { GetResourceStructure } from "@antelopejs/interface-dms-builder";
const result = await GetResourceStructure("product");
if (result.ok) {
const editable = result.data.fields.filter((field) => !field.opaque);
}
ResourceStructure:
| Field | Type | Description |
|---|---|---|
ref | ResourceRef | The resource's ref. |
className | string | The generated table class name. |
tableName | string | The database table name. |
route | string | The DataAPI's base route. |
schema | string | The database schema the table is registered in. |
files | ResourceFiles | The resource's three files. |
fields | ResourceFieldStructure[] | The fields, in declaration order. |
routes | ResourceRoute[] | The exposed routes when the resource serves a subset; omitted when it serves the full set. Best-effort: absent for a hand-edited route map the engine could not map back to semantic names. |
version | string | A content hash of both generated files. |
ResourceFiles names the three paths a resource is composed of: database, dataApi and index.
ResourceFieldStructure is a Partial<FieldAspects> plus:
| Field | Type | Description |
|---|---|---|
name | string | The field name. Always present. |
opaque | boolean | true when the field's decorator stack could not be reversed. |
opaqueReason | string | Why it is opaque, e.g. an unsupported decorator or a missing @Column stack. Set when opaque === true. |
A cleanly-reversed field carries its aspects; an opaque one carries only name and opaqueReason. Read opaque before offering a field for editing.
Delete a Resource
DeleteResource(ref, opts?) deletes the three generated files, unwires every import of them, removes the folder if it is now empty, and — by default — clears the resource's database table so a later resource reusing the name re-seeds cleanly instead of inheriting stale documents.
import { DeleteResource } from "@antelopejs/interface-dms-builder";
const result = await DeleteResource("product");
if (result.ok) {
for (const warning of result.warnings ?? []) {
console.log(warning.code, warning.message);
}
}
DeleteResourceOpts extends MutationOpts:
| Option | Type | Description |
|---|---|---|
keepData | boolean | Leave the resource's table data in place instead of deleting every row. |
expectedVersion | string | The ResourceStructure.version you last read. A mismatch fails with stale. |
When the rows cannot be dropped, the operation still succeeds and attaches a data_not_dropped warning naming the reason — the source is gone, the data may not be.
import { DeleteResource } from "@antelopejs/interface-dms-builder";
await DeleteResource("product", { keepData: true });
Next Steps
- Queries - Derive counts and aggregates from a resource and serve them from a page