Database
Prerequisites
Database operations require the @antelopejs/interface-database-decorators package. Install it in your module and import from @antelopejs/interface-database-decorators.
npm install @antelopejs/interface-database-decorators
yarn add @antelopejs/interface-database-decorators
pnpm add @antelopejs/interface-database-decorators
All table classes, decorators, modifiers, and model functions referenced in this guide come from this package. No additional dependencies are needed.
Defining Tables
You define tables as classes that extend the Table base class. The @RegisterTable decorator registers the class with the database system under a specific table name and schema. Each property on the class represents a column.
import { Table, RegisterTable, Index } from "@antelopejs/interface-database-decorators";
@RegisterTable("users", "main")
class User extends Table {
@Index()
declare email: string;
declare name: string;
declare age: number;
}
export { User };
The first argument to @RegisterTable is the table name and the second is the schema name. Default values on properties define the column types and provide fallback values for new records. The @Index decorator marks a property as indexed for faster lookups.
Table is a base class, not a decorator. Always extend it with class MyTable extends Table.Indexing
The @Index decorator marks a property for database indexing. Indexes speed up queries that filter or sort by the indexed column. You can group multiple columns into a composite index by specifying a group name.
import { Table, RegisterTable, Index } from "@antelopejs/interface-database-decorators";
@RegisterTable("products", "main")
class Product extends Table {
@Index()
declare sku: string;
@Index({ group: "category_brand" })
declare category: string;
@Index({ group: "category_brand" })
declare brand: string;
declare name: string;
declare price: number;
}
export { Product };
Properties with the same group value form a composite index. Queries that filter by both category and brand benefit from the composite index.
Table Modifiers
Modifiers add specialized behavior to table properties. Apply modifiers through Table.with(), which returns an extended base class with the modifier capabilities enabled. You can chain multiple modifiers together.
Hashing
The HashModifier enables the @Hashed decorator for properties that store hashed values, such as passwords. Hashed values are one-way transformed before storage and cannot be reversed.
import { Table, RegisterTable, Index, Hashed } from "@antelopejs/interface-database-decorators";
import { HashModifier } from "@antelopejs/interface-database-decorators";
@RegisterTable("users", "main")
class User extends Table.with(HashModifier) {
@Index()
declare email: string;
@Hashed()
declare password: string;
declare name: string;
}
export { User };
When a record is inserted or updated, the password field is automatically hashed before it reaches the database. The original plaintext value is never stored.
Encryption
The EncryptionModifier enables the @Encrypted decorator for properties that must be stored encrypted. Unlike hashing, encrypted values can be decrypted when read back.
import { Table, RegisterTable, Index, Encrypted } from "@antelopejs/interface-database-decorators";
import { EncryptionModifier } from "@antelopejs/interface-database-decorators";
@RegisterTable("users", "main")
class User extends Table.with(EncryptionModifier) {
@Index()
declare email: string;
@Encrypted({ secretKey: "your-32-char-secret-key-here!!" })
declare socialSecurityNumber: string;
declare name: string;
}
export { User };
Encrypted fields are transparently encrypted on write and decrypted on read. The secretKey option is required and specifies the encryption key. You can also configure the algorithm (defaults to aes-256-gcm) and ivSize (defaults to 16).
Combining Modifiers
You can apply multiple modifiers at once by passing them all to Table.with(). This combination enables both @Hashed and @Encrypted decorators on the same table class.
import { Table, RegisterTable, Index, Hashed, Encrypted } from "@antelopejs/interface-database-decorators";
import { HashModifier, EncryptionModifier } from "@antelopejs/interface-database-decorators";
@RegisterTable("users", "main")
class User extends Table.with(HashModifier, EncryptionModifier) {
@Index()
declare email: string;
@Hashed()
declare password: string;
@Encrypted({ secretKey: "your-32-char-secret-key-here!!" })
declare socialSecurityNumber: string;
declare name: string;
}
export { User };
Creating a Database Schema Instance
Before your models can interact with the database, you must create a schema instance. The CreateDatabaseSchemaInstance function initializes the schema that your tables are registered under.
import { CreateDatabaseSchemaInstance } from "@antelopejs/interface-database-decorators";
await CreateDatabaseSchemaInstance("main");
The schema name passed here must match the schema name used in your @RegisterTable decorators. Call this function once during module initialization (for example, in the module's construct function). The function is async, so make sure to await it.
Data Models
The BasicDataModel function generates a model class with built-in CRUD operations for a registered table. It accepts the table class and returns a new class that you can extend with custom methods.
import { BasicDataModel, GetModel } from "@antelopejs/interface-database-decorators";
import { User } from "../tables/user";
class UserModel extends BasicDataModel(User) {
async getUserByEmail(email: string) {
return this.table.filter({ email }).run();
}
}
export function getUserModel() {
return GetModel(UserModel);
}
export { UserModel };
BasicDataModel is a function that returns a class, not a decorator. Use extends BasicDataModel(TableClass) to create your model. Model instances require a database connection, so use GetModel(YourModel) to obtain a properly initialized instance rather than calling new directly.The returned model includes these built-in instance methods:
| Method | Description |
|---|---|
get(id) | Retrieve a single record by its primary key |
getBy(index, ...keys) | Retrieve records matching a given index |
getAll() | Retrieve all records |
insert(data) | Create a new record |
update(id, data) | Update an existing record by ID |
delete(id) | Remove a record by ID |
The model also provides static methods for data conversion:
| Static Method | Description |
|---|---|
fromPlainData() | Create a model instance from plain data |
fromDatabase() | Create a model instance from database format |
toDatabase() | Convert a model instance to database format |
CRUD Operations
Once you have a model instance, all standard CRUD operations are available as async methods. Each method handles serialization, modifier processing (hashing, encryption), and database communication automatically.
import { getUserModel } from "./models/user-model";
const userModel = getUserModel();
// Create
await userModel.insert({
name: "Alice",
email: "[email protected]",
password: "secret",
});
// Read all
const allUsers = await userModel.getAll();
// Read by index
const users = await userModel.getBy("email", "[email protected]");
const user = users[0];
// Read by primary key
const found = await userModel.get(user._id);
// Update
await userModel.update(user._id, { name: "Alice Updated" });
// Delete
await userModel.delete(user._id);
When inserting a record with a @Hashed property, the value is hashed before storage. When reading a record with an @Encrypted property, the value is decrypted automatically.
Fixtures
The @Fixture decorator provides seed data for development and testing. It takes a generator function that returns an array of records to insert when the table is first created.
import { Table, RegisterTable, Fixture } from "@antelopejs/interface-database-decorators";
@Fixture(() => [
{ name: "admin", permissions: ["read", "write", "delete"] },
{ name: "editor", permissions: ["read", "write"] },
{ name: "viewer", permissions: ["read"] },
])
@RegisterTable("roles", "main")
class Role extends Table {
declare name: string;
permissions: string[] = [];
}
export { Role };
Fixtures run once during schema initialization. They are useful for populating lookup tables, default roles, or any data that must exist before the application starts serving requests.