Table Modifiers
Built-in Modifiers
EncryptionModifier
The EncryptionModifier encrypts field values before storing them and decrypts them on retrieval. Both operations happen transparently.
The default aes-256-gcm algorithm requires a key of exactly 32 bytes. secretKey is passed to Node's cipher as a UTF-8 string, without hex or base64 decoding. Supply a high-entropy, 32-byte UTF-8 value from a secret manager, keep it stable for existing ciphertext, and fail startup if it is missing or invalid; a 64-character hex encoding is not a 32-byte key for this API.
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 UserCredentials extends Table.with(EncryptionModifier) {
@Encrypted({
secretKey,
algorithm: "aes-256-gcm",
ivSize: 16,
})
@Field("string")
declare creditCardNumber: string;
@Encrypted({ secretKey })
@Field("any")
declare personalData: {
ssn: string;
birthDate: string;
};
}
How It Works
- Data is encrypted using the specified algorithm before database insertion.
- A random initialization vector (IV) is generated for each write.
- For authenticated encryption algorithms (e.g.,
aes-256-gcm), an auth tag is stored alongside the ciphertext. - On retrieval, the data is automatically decrypted.
- Date objects are restored as
Dateinstances, but the encoder usestoUTCString(), which drops milliseconds. Encrypted dates round-trip only to whole-second precision.
Options
| Option | Type | Default | Description |
|---|---|---|---|
secretKey | string | (required) | The encryption key. |
algorithm | string | "aes-256-gcm" | Encryption algorithm. |
ivSize | number | 16 | Initialization vector size in bytes. |
HashModifier
The HashModifier stores a salted one-way hash of field values. Unlike encryption, hashing is irreversible -- you cannot retrieve the original value. Its default SHA-256 hash is fast, not a password-hardening function; use a dedicated password hashing library for production credentials.
import {
Table,
Field,
HashModifier,
Hashed,
} from "@antelopejs/interface-database-decorators";
class User extends Table.with(HashModifier) {
@Field("string")
declare email: string;
@Hashed({ algorithm: "sha256" })
@Field("string")
declare password: string;
verifyPassword(plainPassword: string): boolean {
return this.testHash("password", plainPassword);
}
}
How It Works
- A random salt is generated and stored in the field's metadata on first write.
- The value is hashed with the salt using the specified algorithm.
- The
testHashmethod (added by the mixin) hashes a candidate value with the stored salt and compares the result.
Options
| Option | Type | Default | Description |
|---|---|---|---|
algorithm | string | "sha256" | Hashing algorithm. |
Mixin Method
The HashModifier mixin adds a testHash method to table instances:
testHash(field: string, value: unknown): boolean
Returns true if value, when hashed, matches the stored hash for the given field.
LocalizationModifier
The LocalizationModifier stores multiple language versions of a field value. Access a specific locale by calling localize() on the table instance.
import {
Table,
Field,
LocalizationModifier,
Localized,
} from "@antelopejs/interface-database-decorators";
class Product extends Table.with(LocalizationModifier) {
@Field("number")
declare price: number;
@Localized({ fallbackLocale: "en" })
@Field("string")
declare name: string;
@Localized({ fallbackLocale: "en" })
@Field("string")
declare description: string;
}
Usage
const product = new Product();
product._id = "prod-123";
product.price = 99.99;
// Set localized content
product.localize("en").name = "Premium Headphones";
product.localize("fr").name = "Casque Premium";
product.localize("es").name = "Auriculares Premium";
// Read localized content
const spanish = product.localize("es");
console.log(spanish.name); // "Auriculares Premium"
// Fallback to default locale when translation is missing
const german = product.localize("de");
console.log(german.name); // "Premium Headphones" (fallback)
Wildcard Key
The wildcard key "*" retrieves or sets all translations at once at runtime. localize() does not change the property's TypeScript type, so a field declared as string needs an explicit view for bulk access:
// Get all translations
const translations = product.localize("*") as unknown as {
name: Record<string, string>;
};
const allNames = translations.name;
// { en: "Premium Headphones", fr: "Casque Premium", es: "Auriculares Premium" }
// Set all translations
translations.name = {
en: "Premium Headphones",
fr: "Casque Premium",
es: "Auriculares Premium",
de: "Premium-Kopfhoerer",
};
Options
| Option | Type | Description |
|---|---|---|
fallbackLocale | string | Locale to use when the requested locale is unavailable. |
Mixin Method
The LocalizationModifier mixin adds a localize method:
localize(locale: string, fields?: Array<keyof this>): this
localize() changes the active locale and returns the same object, not a locale-specific copy.
- References share state.
spanish,german, andproductabove refer to one object. A laterlocalize()call changes what all three references read. - Fields can be restricted. When you supply
fields, only those fields are unlocked for the selected locale. - Copy values before switching. Save the values you need before changing locales, and do not share one instance between concurrent locale-specific operations.
AutoDateModifier
The AutoDateModifier automatically populates timestamp fields during insert and update operations. Use the CreationTime and UpdateTime decorators to mark fields.
import {
Table,
CreationTime,
UpdateTime,
} from "@antelopejs/interface-database-decorators";
class Article extends Table {
declare title: string;
@CreationTime()
declare creationDate: Date;
@UpdateTime()
declare updateDate: Date;
}
How It Works
- On insert, both
CreationTimeandUpdateTimefields are set to the current date. - On update,
UpdateTimefields are refreshed whileCreationTimefields are removed from the payload so the original creation date is preserved. - This is an event-only modifier: it does not transform stored values and does not require adding a mixin through
Table.with().
Combine Multiple Modifiers
Pass multiple modifier classes to Table.with():
import {
Table,
Field,
EncryptionModifier,
HashModifier,
LocalizationModifier,
Encrypted,
Hashed,
Localized,
} 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 UserProfile extends Table.with(
EncryptionModifier,
HashModifier,
LocalizationModifier,
) {
@Encrypted({ secretKey })
@Field("any")
declare privateInfo: {
address: string;
phoneNumber: string;
};
@Hashed()
@Field("string")
declare password: string;
@Localized({ fallbackLocale: "en" })
@Field("string")
declare bio: string;
}
Create Custom Modifiers
OneWayModifier
A OneWayModifier transforms data in one direction (when writing to the database). Use it for irreversible transformations like hashing.
import {
OneWayModifier,
MixinSymbol,
attachModifier,
} from "@antelopejs/interface-database-decorators";
import { MakePropertyDecorator } from "@antelopejs/interface-core/decorators";
import { createHash } from "crypto";
class PrefixHashModifier extends OneWayModifier<
string | undefined,
[],
{},
{ algorithm?: string }
> {
public readonly autolock = true;
public override lock(_locked: string | undefined, value: unknown) {
if (value === undefined) return undefined;
return createHash(this.options.algorithm || "sha256")
.update(JSON.stringify(value))
.digest("hex");
}
[MixinSymbol] = class {};
}
const PrefixHashed = MakePropertyDecorator(
(target, propertyKey, options?: { algorithm?: string }) => {
attachModifier(
target.constructor,
PrefixHashModifier,
propertyKey,
options || {},
);
},
);
The generic parameters for OneWayModifier<LockedType, Args, Meta, Options> are:
| Parameter | Description |
|---|---|
LockedType | The type stored in the database after transformation |
Args | Additional arguments for lock and test |
Meta | Per-field metadata stored alongside the value |
Options | Configuration passed via the decorator |
TwoWayModifier
A TwoWayModifier extends OneWayModifier with an unlock method for reversible transformations like encryption:
import {
TwoWayModifier,
MixinSymbol,
attachModifier,
} from "@antelopejs/interface-database-decorators";
import { MakePropertyDecorator } from "@antelopejs/interface-core/decorators";
import type { ValueProxy } from "@antelopejs/interface-database";
class Base64Modifier extends TwoWayModifier<
string | undefined,
[],
{},
{ prefix?: string }
> {
public readonly autolock = true;
public readonly autounlock = true;
public override lock(_locked: string | undefined, value: unknown) {
if (value === undefined) return undefined;
const prefix = this.options.prefix || "b64-";
return `${prefix}${Buffer.from(JSON.stringify(value)).toString("base64")}`;
}
public override unlock(locked: string | undefined): unknown {
if (locked === undefined) return undefined;
const prefix = this.options.prefix || "b64-";
const encoded = locked.substring(prefix.length);
return JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
}
public override unlockrequest(
data: ValueProxy<string | undefined>,
): ValueProxy<unknown> {
return data.cast<unknown>();
}
[MixinSymbol] = class {};
}
const Base64Encoded = MakePropertyDecorator(
(target, propertyKey, options?: { prefix?: string }) => {
attachModifier(
target.constructor,
Base64Modifier,
propertyKey,
options || {},
);
},
);
Base64 is encoding, not encryption. This example's unlockrequest() leaves the encoded value unchanged in database expressions; only instance reads decode it.
ContainerModifier
For modifiers that store multiple values in a key-value structure (like LocalizationModifier), extend ContainerModifier. It provides built-in support for the wildcard key "*" for bulk operations:
import { ContainerModifier } from "@antelopejs/interface-database-decorators";
class MyContainerModifier extends ContainerModifier<{}, { fallback?: string }> {
// Override unlock() to add fallback behavior
// Override unlockrequest() for query-time data access
}
Modifier Events
Modifiers can hook into the data lifecycle by implementing event handler methods. These methods are called during data conversion and database operations:
| Event | Triggered When |
|---|---|
fromPlainData | After converting plain data to an instance |
toPlainData | After converting an instance to plain data |
fromDatabase | After reading from the database |
toDatabase | Before writing to the database |
insert | Before inserting a new record |
update | Before updating an existing record |
import {
Modifier,
MixinSymbol,
} from "@antelopejs/interface-database-decorators";
class TimestampModifier extends Modifier {
public insert(instance: any, field: string) {
if (field === "createdAt" && !instance[field]) {
instance[field] = new Date();
}
}
public update(instance: any, field: string) {
if (field === "updatedAt") {
instance[field] = new Date();
}
}
[MixinSymbol] = class {};
}
Event-only modifiers (without lock/unlock) are valid and useful for side effects like logging, validation, or auto-populating fields.
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.
Data Models
Data models provide a high-level interface for interacting with database tables. They handle conversion between database records and TypeScript class instances, and expose methods for common CRUD operations.