Table Definitions
The Table Class
The Table class serves as the base class for all table definitions. Every table automatically includes an _id field as the primary key.
import { Table, Field } from "@antelopejs/interface-database-decorators";
class User extends Table {
@Field("string")
declare name: string;
@Field("string")
declare email: string;
}
Extend with Modifiers
The Table.with() static method incorporates modifier mixins into the table class, adding capabilities like encryption, hashing, or localization:
import {
Table,
Field,
EncryptionModifier,
Encrypted,
} from "@antelopejs/interface-database-decorators";
const secretKey = process.env.ENCRYPTION_KEY;
if (!secretKey || Buffer.byteLength(secretKey, "utf8") !== 32) {
throw new Error("ENCRYPTION_KEY must contain exactly 32 UTF-8 bytes");
}
class SensitiveData extends Table.with(EncryptionModifier) {
@Field("string")
declare publicContent: string;
@Encrypted({ secretKey })
@Field("string")
declare secureContent: string;
}
Provide a high-entropy key through your secret manager; do not use a default key. See Table Modifiers for key format and date precision constraints.
The Index Decorator
The Index decorator marks a field as a database index. Indexed fields enable efficient lookups through getAll() and between() on the underlying AQL table.
import { Table, Field, Index } from "@antelopejs/interface-database-decorators";
class User extends Table {
@Index()
@Field("string")
declare email: string;
@Field("string")
declare name: string;
}
Options
| Option | Type | Description |
|---|---|---|
group | string | Assign the field to a named compound index group. |
crossInstance | boolean | Also keep the index fast for queries that run across all instances. |
Cross-Instance Indexes
Every index is scoped to the schema instance, so queries on a single instance always use it. Set crossInstance: true when the index is also queried across all instances (queries on the CROSS_INSTANCE instance), so the database driver maintains an additional index for those queries.
The option never changes query results. Without it, cross-instance queries that name the index still return the same rows but run slower, and the driver logs a warning once per index. Leave it unset for indexes that are only queried within one instance, since each extra index costs storage and write time.
class User extends Table {
@Index({ crossInstance: true })
@Field("string")
declare email: string;
}
For a compound index, setting crossInstance: true on any field of the group applies it to the whole group.
Compound Indexes
Assign multiple fields to the same index group to create a compound index:
import {
Table,
Field,
Index,
Relation,
} from "@antelopejs/interface-database-decorators";
class User extends Table {
@Field("string")
declare name: string;
}
class UserActivity extends Table {
@Index({ group: "user_action" })
@Field("string")
@Relation({ to: () => User })
declare userId: string;
@Index({ group: "user_action" })
@Field("string")
declare action: string;
@Field("date")
declare timestamp: Date;
}
The Field Decorator
@Field(type) declares the field type for a property. The type is forwarded verbatim into the TableDefinition.fields map handed to the adapter. It accepts the full FieldType union -- primitive tokens ("string", "number", ...), arrays, nested records, or an io-ts codec.
import * as t from "io-ts";
import {
Table,
Field,
Index,
RegisterTable,
} from "@antelopejs/interface-database-decorators";
@RegisterTable("orders", "shop")
class Order extends Table {
@Field("string")
declare id: string;
@Field(t.union([t.literal("open"), t.literal("closed")]))
declare status: "open" | "closed";
@Index()
@Field("number")
declare total: number;
}
Properties without @Field are omitted from fields. @Field stacks freely with @Index.
The Relation Decorator
The Relation decorator declares a link from a field to another table. It is declarative metadata only: database implementations do not enforce it (no foreign key constraint, no referential validation). Introspection tooling consumes it to expose the links between tables — for example to render schema diagrams or navigate related records.
import {
Table,
Field,
Index,
Relation,
} from "@antelopejs/interface-database-decorators";
class Comment extends Table {
@Index()
@Field("string")
@Relation({ to: () => Post })
declare postId: string;
@Field(["string"])
@Relation({ to: () => Tag, many: true })
declare tagIds: string[];
}
Options
| Option | Type | Description |
|---|---|---|
to | () => typeof Table | Thunk returning the target table class (lazy to allow forward references). |
toField | string | Target field name. Defaults to the target table's primary key. |
many | boolean | The decorated field holds multiple target keys (many targets per source record). |
@Relation stacks freely with @Field and @Index.
The RegisterTable Decorator
The RegisterTable class decorator associates a table class with a specific table name and schema. This registration is used by RegisterSchema to build the schema definition automatically.
import {
Table,
Field,
Index,
RegisterTable,
} from "@antelopejs/interface-database-decorators";
@RegisterTable("users", "myapp")
class User extends Table {
@Index()
@Field("string")
declare email: string;
@Field("string")
declare name: string;
}
The first argument is the table name in the database, and the second is the schema name.
The Fixture Decorator
The Fixture decorator defines default data to insert when a table is first created. It receives a generator function that produces initial records.
import {
Table,
Field,
Fixture,
} from "@antelopejs/interface-database-decorators";
@Fixture(() => [
{ _id: "admin", name: "Administrator" },
{ _id: "user", name: "Standard User" },
{ _id: "guest", name: "Guest" },
])
class UserRole extends Table {
@Field("string")
declare name: string;
}
The generator function:
- Receives the table class as its parameter.
- Can return a single object, an array of objects, or a
Promiseresolving to either. - Runs only when the table is empty, preventing duplicate inserts on subsequent starts.
Async Generators
@Fixture(async () => {
const defaults = await loadDefaultConfig();
return defaults.map((cfg) => ({
_id: cfg.key,
value: cfg.value,
}));
})
class SystemConfig extends Table {
@Field("string")
declare value: string;
}
Inheritance
Tables support standard class inheritance. Define a base table with common fields and extend it for specific use cases:
import {
Table,
Field,
Index,
Relation,
} from "@antelopejs/interface-database-decorators";
class BaseEntity extends Table {
@Field("date")
declare createdAt: Date;
@Field("date")
declare updatedAt: Date;
}
class User extends BaseEntity {
@Index()
@Field("string")
declare email: string;
@Field("string")
declare firstName: string;
@Field("string")
declare lastName: string;
}
class Post extends BaseEntity {
@Index()
@Field("string")
@Relation({ to: () => User })
declare authorId: string;
@Field("string")
declare title: string;
@Field("string")
declare content: string;
}
Register a Schema
The RegisterSchema function builds a SchemaDefinition from all tables registered for the given schema id (via @RegisterTable), constructs the Schema (which the adapter provisions), binds each table class to that Schema (see Open a Table from its Class), then inserts any fixture data for tables that are empty.
Call it in the module that declares the tables: the binding then follows that module's database, whichever module later uses the classes.
import { RegisterSchema } from "@antelopejs/interface-database-decorators";
await RegisterSchema("myapp");
Parameters
| Parameter | Type | Description |
|---|---|---|
schemaId | string | Schema id matching the @RegisterTable(_, schemaId) declarations. |
RegisterSchema returns Promise<void>.
Open a Table from its Class
Table.table() returns the AQL table a class describes, in the schema RegisterSchema bound it to:
@RegisterTable("users", "myapp")
class User extends Table {
declare name: string;
}
await RegisterSchema("myapp");
User.table(); // default instance of "myapp"
User.table("tenant-1"); // instance "tenant-1" (CROSS_INSTANCE is accepted too)
User.table(db); // db.table("users") on a SchemaInstance you already hold
When given a SchemaInstance, table() takes the table from it as is: pass an instance of the class's own schema. A class that no RegisterSchema call bound throws, naming the class.