Data Types
Overview
A data type is the single object that answers everything the CMS needs to know about one value: which input component edits it, which Zod schema validates it, which comparison modes its filter offers, and whether the field it sits on needs extra decorators. FormField.type and ColumnOptions.type both take one, which is why a form field and a table column of the same type behave identically.
import {
CreateDataType,
DataType,
DefaultDataCompareTypes,
DefaultDataTypes,
RegisterDataType,
serializeType,
} from "@antelopejs-private/cms/interfaces/cms-base/data-types";
The data-types subpath re-exports the built-in catalogues, so DefaultDataTypes, DefaultDataCompareTypes and StatusType are all reachable from it. They also have their own modules, data-types/default-types and data-types/status-type, which is how much of the CMS imports them.
Note that the data types are not re-exported from the cms-base package root — always import them from the data-types subpath.
Use a Data Type
Instantiate the type with its options and hand the instance to the field or the column.
import { Form } from "@antelopejs-private/cms/interfaces/cms-base";
import { DefaultDataTypes } from "@antelopejs-private/cms/interfaces/cms-base/data-types";
const form = Form({
fields: [
{
id: "name",
label: "Name",
type: new DefaultDataTypes.StringType({ maxLength: 120 }),
required: true,
},
{
id: "status",
label: "Status",
type: new DefaultDataTypes.SelectType({
items: [
{ label: "Draft", value: "draft" },
{ label: "Published", value: "published" },
],
}),
},
],
submitUrl: "/api/articles/new",
});
An instance is reusable: the same object can back a form field and a table column, since it holds only its configuration.
The DataType Contract
abstract class DataType {
constructor(
readonly compareModes: Class<DataCompareMode>[] = [],
readonly defaultCompareMode?: Class<DataCompareMode>,
readonly options?: Record<string, unknown>,
);
protected abstract defaultInputComponent(): ComponentInfoSerialized;
abstract getValidation(): z.ZodType;
filter(context, proxy, key, value, mode, row): ValueProxyOrValue<boolean>;
filterComponents(): Record<string, ComponentInfoSerialized> & { default: ComponentInfoSerialized };
inputComponent(): ComponentInfoSerialized;
decorateField?(target: unknown, key: PropertyKey, descriptor?: PropertyDescriptor): void;
}
| Member | Role |
|---|---|
compareModes | Compare-mode classes this type offers in the filter dropdown. |
defaultCompareMode | The mode selected when the viewer adds a filter on this field. |
options | The type's own configuration, kept on the instance and readable by other code. |
defaultInputComponent() | Abstract. Returns the input component the type renders. Implemented by every concrete type. |
getValidation() | Abstract. Returns the Zod schema for one value of this type, before the field-level adaptation described in Forms. |
inputComponent() | The form input. Defaults to defaultInputComponent(). |
filterComponents() | The filter inputs, keyed by compare-mode id with a mandatory default. Defaults to { default: defaultInputComponent() }. |
filter() | Applies a filter for one compare mode. The base implementation looks the mode up in the compare-mode registry and delegates to it; a type overrides it when a mode needs different semantics. |
decorateField() | Optional. Called by the @Column decorator so the type can add further decorators to the field — this is how the relation types install their join. |
Write a Custom Type
StatusType is the smallest complete example: a boolean rendered as an online/offline indicator whose labels and colours are configurable.
import { z } from "zod";
import { FormComponents } from "@antelopejs-private/cms/interfaces/cms-base/form";
import {
DataType,
DefaultDataCompareTypes,
RegisterDataType,
} from "@antelopejs-private/cms/interfaces/cms-base/data-types";
@RegisterDataType("status")
export class StatusType extends DataType {
constructor(
public readonly options: {
onlineLabel?: string;
offlineLabel?: string;
onlineColor?: string;
offlineColor?: string;
} = {
onlineLabel: "$common.status.online",
offlineLabel: "$common.status.offline",
onlineColor: "primary",
offlineColor: "neutral",
},
) {
super(
[DefaultDataCompareTypes.Is, DefaultDataCompareTypes.IsNot],
DefaultDataCompareTypes.Is,
options as Record<string, unknown>,
);
}
protected defaultInputComponent() {
const component = FormComponents.InputCheckbox();
component.options = this.options;
return component;
}
getValidation() {
return z.boolean();
}
}
Three pieces make it a type: the @RegisterDataType("status") decorator puts it in the registry under an id, the super() call declares the compare modes and stores the options, and the two abstract members supply the input and the schema. StatusType itself is exported by the data-types subpath and is one of the three column types a kanban board can group by — see Tables.
The Registry
RegisterDataType
const RegisterDataType: (id: string) => ClassDecorator<Class<DataType>>;
Registers the decorated class under id, in both directions: CreateDataType looks up the class by id, and getDataTypeId recovers the id from an instance. An id must be unique — registering twice under the same string replaces the earlier class.
RegisterDataCompareMode
const RegisterDataCompareMode: (
id: string,
noInput?: boolean,
) => ClassDecorator<Class<DataCompareMode>>;
Registers a compare mode. The decorator instantiates the class immediately and keeps that single instance, so a compare mode is stateless. noInput marks a mode that needs no value from the viewer — serializeType replaces its filter component with the literal "noInput".
DataCompareMode is the interface a mode implements:
interface DataCompareMode {
filter(
context: RequestContext,
proxy: ValueProxy<unknown>,
key: string,
value: unknown,
row: ValueProxy<Record<string, unknown>>,
): ValueProxyOrValue<boolean>;
}
proxy is the database value proxy for the field being filtered and row the proxy for the whole row, both from @antelopejs/interface-database, so a mode builds a database-side predicate rather than filtering in memory.
CreateDataType
function CreateDataType(id: string, options?: unknown): DataType;
Instantiates a registered type by id, forwarding options to its constructor. It asserts on an unknown id with the message Unknown type <id>. Use it when the type is chosen at runtime — from configuration, or from a description produced by another module — rather than written in the source.
getDataTypeId
function getDataTypeId(instance: DataType): string | undefined;
Returns the id an instance's class was registered under, or undefined when the class is not registered. It resolves the class from the instance's own prototype, so a subclass of a registered type returns its own id, not its parent's.
Serialization
function serializeType(instance?: DataType): DataTypeSerialized | undefined;
serializeType is what turns a type instance into the description shipped to the frontend. It returns undefined when there is no instance, and also when the instance's class is not registered.
DataTypeSerialized:
| Field | Type | Description |
|---|---|---|
id | string | The registered id. |
compareModes | string[] | Ids of the type's compare modes; unregistered ones are dropped. |
defaultCompareMode | string | Id of the default mode. |
filterComponents | Record<string, FilterComponent> | Filter inputs by mode id, from filterComponents(), with every noInput mode's entry replaced by the literal "noInput". |
inputComponent | ComponentInfoSerialized | The form input, from inputComponent(). |
FilterComponent is ComponentInfoSerialized | "noInput". The value-level counterpart, ComponentOrNoInput, is described in Shared Types.
Compare Modes
DefaultDataCompareTypes holds the built-in modes. Each entry is a class; you reference the class in a type's compareModes, and the string id is what appears in a serialized type and in a filter query.
| Class | Id | Input |
|---|---|---|
Is | is | value |
IsNot | is_not | value |
GreaterThan | greater_than | value |
GreaterThanOrEqualTo | greater_than_or_equal_to | value |
LessThan | less_than | value |
LessThanOrEqualTo | less_than_or_equal_to | value |
IsBetween | is_between | two values |
IsEmpty | is_empty | none |
IsNotEmpty | is_not_empty | none |
Include | include | value |
Exclude | exclude | value |
Contains | contains | value |
NotContains | not_contains | value |
ArrayContainsString | array_contains_string | value |
IsEmpty and IsNotEmpty are the two registered with noInput. The namespace also exports escapeRegex(str: string): string, the helper the substring modes use to make a viewer-supplied value safe inside a pattern.
The Default Types
DefaultDataTypes is the built-in catalogue. Every class takes its options object as its only constructor argument, and most of them accept a fallback string used in place of an empty value.
| Class | Id | Compare modes (default first) | Input |
|---|---|---|---|
StringType | string | contains, is, is_not, not_contains, is_empty, is_not_empty | text or textarea |
NumberType | number | is, is_not, greater_than, greater_than_or_equal_to, less_than, less_than_or_equal_to, is_empty, is_not_empty | number |
PriceType | price | as NumberType | number |
PercentageType | percentage | as NumberType | percentage |
DateType | date | is, less_than, greater_than, is_between, is_empty, is_not_empty | date picker |
BooleanType | boolean | is, is_not, is_empty, is_not_empty | switch |
SelectType | select | include/exclude when multiple, otherwise is/is_not; plus is_empty, is_not_empty | select |
EmailType | email | contains, is, is_not, not_contains, is_empty, is_not_empty | |
UrlType | url | is, is_not, is_empty, is_not_empty | text |
PhoneType | phone | contains, is, is_not, not_contains, is_empty, is_not_empty | phone |
ColorType | color | is, is_not, is_empty, is_not_empty | colour |
PasswordType | password | none | password |
RichTextType | rich_text | none | rich text |
StringTimeType | string_time | as NumberType | time |
TreeType | tree | include, exclude | tree |
PermissionsType | permissions | include, exclude | permissions tree |
RelationType | relation | is, is_not, is_empty, is_not_empty | relation picker |
CascaderRelationType | cascader_relation | is, is_not, is_empty, is_not_empty | cascader |
AddressType | address | is_empty, is_not_empty | address |
FileType | file | none | file upload |
ImageType | image | none | image upload |
Scalar Types
| Options interface | Fields |
|---|---|
StringTypeOptions | placeholder?, maxLength?, minLength?, textarea?, rows?, fallback? |
NumberTypeOptions | min?, max?, step?, placeholder?, fallback? |
DateTypeOptions | range?, multiple?, minDate?, maxDate?, fallback? |
BooleanTypeOptions | fallback? |
SelectTypeOptions | items: FormComponents.SelectOption[], placeholder?, multiple?, deselectable?, fallback? |
EmailTypeOptions, ColorTypeOptions, UrlTypeOptions, RichTextTypeOptions | placeholder?, fallback? |
PasswordTypeOptions | placeholder?, minLength?, confirmPassword?, confirmPlaceholder?, fallback? |
PhoneTypeOptions | placeholder?, requiredPrefix?, fallback? |
StringTimeTypeOptions | placeholder?, min?, max?, fallback? |
TreeTypeOptions | items?: TreeNode[], fetchUrl?, placeholder?, multiple?, fallback? |
PermissionsTypeOptions | fetchUrl?, fallback? |
Notes on the ones whose behaviour is not obvious from the options:
StringTyperenders a textarea instead of a single-line input whentextareais set, and passesrowsto it.DateTypeoverridesfiltersois,is_notandis_betweencompare whole UTC days rather than instants, and adds a range picker as the filter component foris_between. Its schema accepts aDateor an ISO-shaped string and transforms it to aDate; withrangeormultiplethe value becomes an array.BooleanTypeuses a two-option select as its filter component, so a viewer picks "checked"/"unchecked" rather than typing a value.SelectTypevalidates against thevalues of itsitems, as an enum or an array of that enum whenmultipleis set.PriceTypeis aNumberTypewhose options default tomin: 0andstep: 0.01.PercentageTypeis aNumberTypewhose options default tomin: 0,max: 1andstep: 0.01; the stored value is a fraction while the input works in whole percent.PhoneTypeonly validates the E.164 pattern whenrequiredPrefixis set; otherwise any string passes.ColorTypevalidates a six-digit#RRGGBBstring.StringTimeTypevalidates a number — a time of day expressed as a number, bounded byminandmax.
Relations
RelationType<T> points a field at another data controller. Its input is a searchable picker whose searchUrl is the target controller's select endpoint, and — unless filterOnly is set — its decorateField installs a data-api Foreign join so list rows carry the resolved related row instead of the raw foreign key.
import { DefaultDataTypes } from "@antelopejs-private/cms/interfaces/cms-base/data-types";
import { Column } from "@antelopejs-private/cms/interfaces/cms-base/table-view";
@Column({
name: "Customer",
type: new DefaultDataTypes.RelationType({
dataApiController: customerDataAPI,
keyMapping: { label: "name", value: "_id" },
}),
filterable: true,
})
declare customerId: string;
| Option | Type | Description |
|---|---|---|
dataApiController | T extends ControllerClass | The target data controller. Required. |
keyMapping | { label?, value?, avatar?, disabled? } | Which of the target's fields fill each role in the picker. Keys are checked against the target's own field names. |
index | string | Index the join reads, when it is not the primary key. |
placeholder | string | Picker placeholder. |
multiple | boolean | Allow more than one related row. |
deselectable | boolean | Allow clearing the selection. |
filterOnly | boolean | Skip the join entirely: the raw scalar id stays in the list output and the funnel filter runs equality on it. |
fallback | string | Text used in place of an empty value. |
When the target controller exposes a new endpoint and has form fields, the picker also carries an inline "add" form built from the target's new fields, plus the permission id of the target's add action.
CascaderRelationType<T> is the same idea for a self-referencing table: the target's select endpoint returns a flat list, and the widget rebuilds the tree client-side. Its keyMapping is required and adds parent — the field pointing at the parent row, which must itself carry @Select() — alongside the required label and value. It adds maxDepth and leafOnly to the relation options, and supports the same filterOnly behaviour.
Address
AddressType<T> stores a structured postal address. Its Address shape follows ISO 19160-1 and maps directly onto the UBL cac:PostalAddress used by EN 16931 / Peppol BIS Billing 3.0.
| Field | Required | Description |
|---|---|---|
streetName | yes | Street or thoroughfare name. |
postalCode | yes | Postal or ZIP code. |
city | yes | City or locality. |
countryCode | yes | ISO 3166-1 alpha-2 country code. |
houseNumber | no | House or building number. |
boxNumber | no | Box, unit or apartment number. |
addressLine2 | no | Supplementary delivery information. |
countrySubdivision | no | Region, state or province. |
The type's options are placeholder (one string per address component), autocomplete, an optional dataApiController with a keyMapping of label / value for picking an existing address, and fallback. AddressAutocomplete configures the geocoder:
| Option | Type | Description |
|---|---|---|
enabled | boolean | Enable suggestions on the street field. Defaults to false. |
url | string | Geocoder endpoint. Defaults to "https://photon.komoot.io/api". |
lang | string | Preferred result language, ISO 639-1. Falls back to the UI locale. |
limit | number | Maximum number of suggestions. Defaults to 5. |
placeholder | string | Search field placeholder. |
Files and Images
FileTypeOptions has multiple?, constraints?: UploadConstraints, path?, storage? and fallback?. ImageTypeOptions has multiple?, max?, constraints?, path?, storage? and resize?, the FormComponents.ImageResizeOptions the input applies client-side before uploading (Forms). constraints is the UploadConstraints type from @antelopejs/interface-file-storage; storage names the storage backend and path the prefix uploads land under.
A file value is the resource key as a string; the schema validates it against the constraints by reading the uploaded object's metadata, matching allowedMimetypes patterns including the type/* form. A key pointing at no real upload fails validation like any other invalid input, while a metadata read that fails for another reason — a storage outage, an auth error — propagates as a server error rather than being reported as a bad field. An image value is an ImageValue — { key: string; alt?: string; principal?: boolean } — and ImageType defaults allowedMimetypes to ["image/*"], caps the array at max when multiple is set, and rejects a list with more than one principal image.
Both types are what makes the table view's file handling apply to a column: staged uploads are promoted on write and orphaned objects deleted after an edit or a delete. The helpers behind that are described in Helpers.
Next Steps
- Helpers - Search, export jobs, file references and the archive-mode defaults.