[data-reveal]{opacity:1!important;transform:none!important}
Table Modifiers
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.

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 Date instances, but the encoder uses toUTCString(), which drops milliseconds. Encrypted dates round-trip only to whole-second precision.

Options

OptionTypeDefaultDescription
secretKeystring(required)The encryption key.
algorithmstring"aes-256-gcm"Encryption algorithm.
ivSizenumber16Initialization 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 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 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

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

localize() changes the active locale and returns the same object, not a locale-specific copy.

  • References share state. spanish, german, and product above refer to one object. A later localize() 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 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";

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:

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";
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:

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.