[data-reveal]{opacity:1!important;transform:none!important}
Table Definitions
Database Decorators

Table Definitions

The Database Decorators package provides a class-based approach to defining database tables. You extend the Table base class, declare each field with TypeScript's declare keyword and the @Field decorator, and use further decorators to configure indexes, relations, and initial data.

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

OptionTypeDescription
groupstringAssign the field to a named compound index group.
crossInstancebooleanAlso 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

OptionTypeDescription
to() => typeof TableThunk returning the target table class (lazy to allow forward references).
toFieldstringTarget field name. Defaults to the target table's primary key.
manybooleanThe 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 Promise resolving 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

ParameterTypeDescription
schemaIdstringSchema 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.