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

Table Modifiers

Table modifiers transform field values as they are stored in and retrieved from the database. They enable transparent encryption, hashing, localization, and custom data transformations. Add modifiers to a table class using the Table.with() method.

Built-in Modifiers

EncryptionModifier

The EncryptionModifier encrypts field values before storing them and decrypts them on retrieval. Both operations happen transparently.

import { Table, Field, EncryptionModifier, Encrypted } from "@antelopejs/interface-database-decorators";

class UserCredentials extends Table.with(EncryptionModifier) {
  @Encrypted({
    secretKey: process.env.ENCRYPTION_KEY || "default-key",
    algorithm: "aes-256-gcm",
    ivSize: 16,
  })
  @Field("string")
  declare creditCardNumber: string;

  @Encrypted({ secretKey: process.env.ENCRYPTION_KEY || "default-key" })
  @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 preserved through JSON serialization with a custom encoder.

Options

OptionTypeDefaultDescription
secretKeystring(required)The encryption key.
algorithmstring"aes-256-gcm"Encryption algorithm.
ivSizenumber16Initialization vector size in bytes.

HashModifier

The HashModifier stores a one-way hash of field values. Unlike encryption, hashing is irreversible -- you cannot retrieve the original value. Use this for passwords and other sensitive data that only needs equality verification.

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 testHash method (added by the mixin) hashes a candidate value with the stored salt and compares the result.

Options

OptionTypeDefaultDescription
algorithmstring"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:

// Get all translations
const allNames = product.localize("*").name;
// { en: "Premium Headphones", fr: "Casque Premium", es: "Auriculares Premium" }

// Set all translations
product.localize("*").name = {
  en: "Premium Headphones",
  fr: "Casque Premium",
  es: "Auriculares Premium",
  de: "Premium-Kopfhoerer",
};

Options

OptionTypeDescription
fallbackLocalestringLocale 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

Sets the active locale for accessing localized fields. If fields is provided, only those fields are unlocked for the specified locale.

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 CreationTime and UpdateTime fields are set to the current date.
  • On update, UpdateTime fields are refreshed while CreationTime fields 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";

class UserProfile extends Table.with(EncryptionModifier, HashModifier, LocalizationModifier) {
  @Encrypted({ secretKey: process.env.SECRET_KEY || "default-key" })
  @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:

ParameterDescription
LockedTypeThe type stored in the database after transformation
ArgsAdditional arguments for lock and test
MetaPer-field metadata stored alongside the value
OptionsConfiguration 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";

class Base64Modifier extends TwoWayModifier<string, [], {}, { 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) {
    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: any) {
    return data;
  }

  [MixinSymbol] = class {};
}

const Base64Encoded = MakePropertyDecorator(
  (target, propertyKey, options?: { prefix?: string }) => {
    attachModifier(target.constructor, Base64Modifier, propertyKey, options || {});
  },
);

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:

EventTriggered When
fromPlainDataAfter converting plain data to an instance
toPlainDataAfter converting an instance to plain data
fromDatabaseAfter reading from the database
toDatabaseBefore writing to the database
insertBefore inserting a new record
updateBefore 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.